Lifecycle, settings and stopping

Python · Updated 09 November 2026

The order of the callbacks, how to pace on server ticks, settings fields and the cooperative stop.

The lifecycle

  1. on_config_update(config) runs once before on_start, with the settings in force. It runs again after each change the user commits, on the script's thread between on_loop calls.
  2. on_start(ctx) runs once. Keep ctx.
  3. on_loop() runs repeatedly. It returns the delay in milliseconds until the next call. A negative value stops the script.
  4. on_stop() runs once at the end.

Each script runs on a thread of its own.

Pace on server ticks

The game state has three counters, and only one of them is for pacing:

  • server_tick: the 0.6 second server tick. Pace against this one. It is -1 until a world is loaded.
  • game_cycle: the client's frame counter, about 20 ms. It is not a timer for scripts.
  • publish_seq: an internal counter. Never compare it with game_cycle.

Pacing off the wrong counter makes a script run about 30 times too fast. Use ctx.sleep_ticks(n), which waits for the server tick to move on by n:

def on_loop(self) -> int:
    ...
    if not self.ctx.sleep_ticks(1):
        return -1
    return 0

sleep_ticks returns False when the script is being stopped, so return a negative value then.

Settings

Declare settings with get_config_fields(). The GUI draws them in the script's panel and saves them per script:

from botwithus.api import BotScript, choice_field, int_field, script_manifest


@script_manifest(name="Cutter", category="Woodcutting")
class Cutter(BotScript):
    def get_config_fields(self):
        return [int_field("logs", "Logs to cut", 28, min=1, max=1000),
                choice_field("tree", "Tree", ["Oak", "Willow", "Yew"], default="Willow")]

    def on_config_update(self, config):
        self.logs, self.tree = config["logs"], config["tree"]

    def on_loop(self) -> int:
        return 600
  • The field kinds are int_field, string_field, bool_field, choice_field and item_id_field. For item_id_field, -1 means "no item".
  • ctx.config holds the same values as a read-only mapping.
  • A script is refused when it is listed or started if a choice default is not one of its options, a choice has no options, an item_id default is below -1, or a string default is longer than 256 bytes without max_len=.
  • From a console (bwu_host --py) the settings are their defaults.

author defaults to "" and shows as "Unknown author". category groups the script in the GUI.

Stopping: cooperative, never forced

When the GUI stops a script:

  • ctx.sleep(ms) and ctx.sleep_ticks(n) return False within 250 ms;
  • no further on_loop runs;
  • on_stop is called.

The whole stop must finish within 10 seconds, or the worker process is ended.

The framework never interrupts your code. A script blocked in time.sleep or in a long call of its own only sees the stop once that call returns. So:

  • wait with ctx.sleep and ctx.sleep_ticks, never time.sleep;
  • check ctx.stopping (or ctx.is_stop_requested()) inside any long loop of your own;
  • call ctx.stop_self() to end the script from anywhere.

If the game client exits, the run ends the same way: no further on_loop, then on_stop.

Next: API tour.