Anatomy of a script

Java · Updated 09 November 2026

BotScript, @ScriptManifest and the onStart, onLoop, onStop lifecycle.

A script is a class that implements com.botwithus.bot.api.BotScript and carries a @ScriptManifest annotation. The package and com.botwithus imports are left out below; A complete example shows a whole file.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ScriptManifest(
    name = "My Script",
    version = "1.0",
    author = "You",
    description = "Does something useful"
)
public class MyScript implements BotScript {

    private static final Logger log = LoggerFactory.getLogger(MyScript.class);
    private ScriptContext ctx;

    @Override
    public void onStart(ScriptContext ctx) {
        this.ctx = ctx;
        log.info("Started!");
    }

    @Override
    public int onLoop() {
        GameAPI api = ctx.getGameAPI();
        GameSnapshot snap = api.snapshot();
        if (snap != null && snap.self() != null) {
            // read state from the snapshot, act through api
        }
        return 1000;
    }

    @Override
    public void onStop() {
        log.info("Stopped.");
    }
}

@ScriptManifest

Field Default Meaning
name required The name shown in the host
version "1.0" Your script's version
author "" Your name
description "" One line about what it does
category UNCATEGORIZED A ScriptCategory, such as UTILITY or WOODCUTTING

If two different scripts use the same name, only one of them is registered, and the host's Installed page lists the other as an older copy. Keep names unique.

The lifecycle

  1. onStart(ScriptContext ctx) runs once. Keep the context and set up your state here.
  2. onLoop() runs again and again. Its return value is the delay in milliseconds before the next call. Return -1 to stop the script.
  3. onStop() runs once at the end, either because onLoop returned -1 or because the user pressed Stop. Clean up here.

If your script declares settings, onConfigUpdate also runs once before the first onLoop (see API tour).

Stopping cleanly from deep inside your code

ctx.stopSelf() asks for the same clean stop as returning -1, from anywhere in your script. The current onLoop finishes, then onStop runs.

If you wait inside onLoop, poll ctx.isStopRequested() and return when it turns true:

@Override
public int onLoop() {
    Bank bank = ctx.getGameAPI().bank();
    while (!bank.isOpen()) {
        if (ctx.isStopRequested()) {
            return -1;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return -1;
        }
    }
    log.info("Bank is open.");
    return 600;
}

A stop, whether from ctx.stopSelf() or the user's Stop button, also interrupts the script's thread. A sleep or wait then ends early with InterruptedException, so that catch is the stop path: restore the interrupt flag and return.

A script that ignores stop requests is eventually cut off: every further game call throws, and the script's thread stays parked for the rest of the session.

Registering the script

The host only sees classes your module provides:

provides com.botwithus.bot.api.BotScript with my.script.MyScript;

If you forget this line, the JAR loads but no script appears, and the host log says:

Module 'my.script' contains no BotScript providers

The fix is always the provides clause in module-info.java.

Next: API tour.