The 600 ms server tick
The game server advances state on ticks of roughly 600 ms. RuneLite invokes each subscribedonGameTick handler once per tick, synchronously on the client thread.
- One meaningful action per tick. Never assume dependent actions (withdraw, equip, close bank) complete in one callback. Dispatch one action, return, and observe the result on a later tick.
- React, never sleep. Do not use
Thread.sleep()or busy-wait loops. Return from the tick handler and let the next tick evaluate the new state. - Observe before you act. Query inventory, widgets, actors, and objects each tick instead of assuming your last click succeeded.
Observed state over remembered state
Stateful scripts get stuck: a flag likeisChopping = true stops matching reality the moment the player is attacked, walks away, or logs out. Query live state each tick and store workflow state only where a pipeline or state machine needs it, reconciled against the game before every action.
The two API surfaces
Every mutating call returns an
InteractionResult with a status of CONFIRMED, ACCEPTED, PACED, or FAILED. Inspect the status at the decision point instead of comparing messages:
Choose the right workflow shape
- A single interaction needs no framework: check the condition, call one
Api.actionsmethod, inspect the result. - A fixed sequence (open bank, withdraw, close) fits TaskPipeline.
- Branching workflows with recovery and terminals fit the carousel state machine or a domain workflow builder.
Runtime gates
The shared runtime,PacketUtilsPlugin, gates every action your plugin dispatches. It stays enabled, owns revision validation, menu dispatch, walker ticking, action pacing, and input locking, and it fails closed when the client, bundled evidence, and reflection shape disagree.
Because pacing and locking are suite-level, plugins never implement their own cooldown timers for shared actions. When your plugin responds to transient state that already limits firing (a dialogue widget only exists while dialogue is open), a per-tick guard is enough:
Break handler contract
Plugins that run full automation register with the shared break handler:- Register on startup, and call
startPluginonly while active automation runs. - Check
shouldBreak()before game interactions; when a break is due, yield. - Release input locks while paused, breaking, or stopped.
- Always-on helper plugins (Dialogue Helper, Upkeep) register no break handler and hold no input lock.
IDs and widgets come from registries
Never hardcode item, NPC, object, or widget identifiers. Resolve them from:net.runelite.api.gameval.ItemIDfor itemsIdMapRegistryfor revision-aware entity and item mappingsWidgetCatalogfor revision-pinned widget discovery
Runtime status and diagnostics
Api.debug.SuiteRuntimeStatus.snapshot() returns a read-only snapshot: the revision health log, expected and live client revisions, walker state, and pacer status. Use it to explain blocked states instead of guessing. The shared sidebar exposes the same information in the Suite status tab.