Design process
Work through the same five questions every time:Walkthrough: bone buryer
A minimal skiller loop: find bones in the inventory, bury one per tick, and wait out the animation.Walkthrough: bank runner with a state machine
A full inventory of willow logs triggers a bank restock throughBankWorkflowBuilder, which manages opening, depositing, withdrawing, and closing:
Walkthrough: stateless dialogue helper
Dialogue widgets exist only while the server presents them. React within the tick and keep no memory across ticks:Pattern: pacing
ActionPacer is suite-wide and driven by Packet Utils. The action APIs check it internally, so never gate your own tick loop on ActionPacer.isReady(...). Packet Utils increments the tick count and applies jitter before your handler runs, so a same-tick isReady() check returns false at the wrong moment and starves your automation.
Pattern: spatial selection
Entity lists are not sorted by distance.first() grabs whatever the raw scan returned and produces unstable target choice. Select by path reachability or by a stable anchor:
QueryResults.nearestTo(...) and sortedByDistanceTo(...) use straight-line tile distance. When reachability decides the action target, use nearestByPath().
Pattern: spell resolution
Spell widgets are revision-backed addresses. Resolve names through the shared resolver backed by theApi.actions.Spell catalog, and treat an empty result as a configuration error:
resolveSpellWidget(...) when the workflow needs the live widget; it stays empty when the spell is not visible in the active spellbook.
Pattern: query freshness
Query builders scan and cache per tick. Caching a builder across ticks reads stale state:Pattern: ordered tick decisions
When several combat policies must run in a fixed order once per tick, useTickDecisionList instead of duplicating lastFiredTick guards. See tick decisions for the full contract.
Loop ergonomics
AutomationLoop wraps the observe-decide-act cycle when a plugin wants loop-style ergonomics while keeping ownership in its own onGameTick:
AutomationLoop.fromPipeline(...) and AutomationLoop.fromStateMachine(...) adapt existing workflows without creating another pacer, walker, or global tick owner.
Read, then act
Pair a read surface with its result-aware write:DiaryProgressApi exposes only explicitly mapped varbit and varplayer-backed tasks. Unknown diary coverage is absent rather than guessed.
The rules reviewers enforce
- Do not sleep. Let the tick dispatcher drive progression.
- Keep dialogue handlers stateless: click what is visible, otherwise do nothing.
- Use an intentional spatial selector:
nearestByPath()for reachable targets,walkable().nearestToPoint(anchor)for a work area. Neverfirst()as a nearest-target policy. - Gate full automation on the break handler; pause actions during planned or active breaks.
- Inspect
InteractionResultat the decision point. Do not compare messages and do not treat dispatch as completion. - Resolve IDs from
gameval.ItemID,IdMapRegistry, andWidgetCatalog. Never hardcode identifiers. - Route every write through
Api.actions.*. Do not call raw packet queues directly.