> ## Documentation Index
> Fetch the complete documentation index at: https://runelite.zip/llms.txt
> Use this file to discover all available pages before exploring further.

# Scripting patterns

> The decision loop, worked examples, and the do-and-avoid patterns that pass review.

This page teaches the event-driven shape of n3Plugins automation. The examples show the decision loop and real API signatures. A production plugin additionally needs registration, configuration, break handler lifecycle, and shutdown cleanup; the [plugin development playbook](/plugins/overview) covers the surrounding skeleton.

## Design process

Work through the same five questions every time:

```mermaid theme={null}
flowchart TD
    A[Define the goal and requirements] --> B[Map the loop states]
    B --> C{Linear or branching?}
    C -->|Linear sequence| D[TaskPipeline]
    C -->|Branching or recovery| E[TypesafeCarouselStateMachine]
    C -->|Simple check-and-react| F[Plain onGameTick loop]
    D & E & F --> G[Add spatial queries and registries]
    G --> H[Add humanizer fidgets and break gates]
```

## Walkthrough: bone buryer

A minimal skiller loop: find bones in the inventory, bury one per tick, and wait out the animation.

```java theme={null}
package com.n3plugins.boneburyer;

import com.n3plugins.Api.actions.InventoryActions;
import com.n3plugins.Api.common.InteractionResult;
import com.n3plugins.sdk.query.Inventory;
import net.runelite.api.Client;
import net.runelite.api.events.GameTick;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import javax.inject.Inject;

public class BoneBuryerPlugin extends Plugin {
    @Inject private Client client;
    private int lastFiredTick = -1;

    @Subscribe
    public void onGameTick(GameTick event) {
        int currentTick = client.getTickCount();
        if (currentTick == lastFiredTick) return;   // run once per server tick

        if (client.getLocalPlayer().getAnimation() != -1) {
            return;                                  // wait out the bury animation
        }

        Inventory.search()
            .withName("Bones")
            .first()
            .ifPresent(bone -> {
                InteractionResult result = InventoryActions.use(bone, "Bury");
                if (result.succeeded()) {
                    lastFiredTick = currentTick;
                }
            });
    }
}
```

## Walkthrough: bank runner with a state machine

A full inventory of willow logs triggers a bank restock through `BankWorkflowBuilder`, which manages opening, depositing, withdrawing, and closing:

```java theme={null}
@Subscribe
public void onGameTick(GameTick event) {
    boolean needsBanking = Inventory.search().withName("Willow logs").count() >= 27;

    if (needsBanking && bankMachine == null) {
        bankMachine = BankWorkflowBuilder.create(
            BankRestockPlan.builder()
                .inventoryLoadout(loadout)
                .build());
    }

    if (bankMachine != null) {
        CarouselResult<BankRestockState> result = bankMachine.pulse(client.getTickCount());

        if (result.getSnapshot().getStatus() == WorkflowStatus.COMPLETED
                || result.getSnapshot().getStatus() == WorkflowStatus.FAILED) {
            bankMachine = null;
        }
    }
}
```

## Walkthrough: stateless dialogue helper

Dialogue widgets exist only while the server presents them. React within the tick and keep no memory across ticks:

```java theme={null}
@Subscribe
public void onGameTick(GameTick event) {
    int currentTick = client.getTickCount();
    if (currentTick == lastFiredTick) return;   // one dialogue click per tick

    if (Dialogue.isPresent()) {
        // Prefer a Quest Helper marked option when one is present
        if (DialogActions.chooseQuestHelperMarkedOption().succeeded()) {
            lastFiredTick = currentTick;
            return;
        }
        DialogActions.continueDialogue();
        lastFiredTick = currentTick;
    }
}
```

## 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.

```java theme={null}
// Avoid: pacer check in your own loop
if (!ActionPacer.isReady(System.currentTimeMillis())) return;
NPCActions.interact("Banker", "Bank");

// Prefer: let the action API pace, react to the result
InteractionResult result = NPCActions.interact("Banker", "Bank");
if (result.getStatus() == InteractionStatus.PACED) {
    // re-evaluate next tick
}
```

## 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:

```java theme={null}
// Avoid: raw memory order
TileObject tree = TileObjects.search().withName("Yew").first().orElse(null);

// Prefer: nearest path-reachable target
TileObject tree = TileObjects.search()
    .withName("Yew")
    .nearestByPath()
    .orElse(null);

// Prefer: stable work area anchored to a landmark
TileObject workAreaTree = TileObjects.search()
    .withName("Yew")
    .walkable()
    .nearestToPoint(workAreaAnchor)
    .orElse(null);
```

`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 the `Api.actions.Spell` catalog, and treat an empty result as a configuration error:

```java theme={null}
Optional<Integer> spellId = MagicActions.resolveSpellInfo("High Level Alchemy");
if (!spellId.isPresent()) {
    return; // unsupported or misspelled name; retry after configuration changes
}

// Cast by enum; the resolver derives the packed widget ID (High Alchemy is
// InterfaceID.MagicSpellbook.HIGH_ALCHEMY).
MagicActions.cast(Spell.HIGH_LEVEL_ALCHEMY, target);
```

Never copy a packed spell widget ID into a plugin or depend on removed widget-constant wrappers. Use `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:

```java theme={null}
// Avoid: a cached query builder
private ItemQuery inventoryQuery;

// Prefer: build the query every tick
int foodCount = Inventory.search().withName("Lobster").count();
```

## Pattern: ordered tick decisions

When several combat policies must run in a fixed order once per tick, use `TickDecisionList` instead of duplicating `lastFiredTick` guards. See [tick decisions](/guides/combat-magic#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`:

```java theme={null}
AutomationLoop loop = new AutomationLoop(
        AutomationLoopConfig.builder(this)
                .tickSupplier(client::getTickCount)
                .requireBootstrap(true)
                .breakGate(() -> breakHandler.shouldBreak(this),
                        () -> breakHandler.isBreakActive(this),
                        () -> breakHandler.startBreak(this))
                .build(),
        context -> {
            InteractionResult result = NPCActions.interact("Banker", "Bank");
            return result.succeeded()
                    ? LoopPulseResult.active(result.getMessage())
                    : LoopPulseResult.waiting(result.getMessage());
        });

@Subscribe
public void onGameTick(GameTick event) {
    loop.pulse();
}
```

`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:

```java theme={null}
Production.findProduct("Shortbow")
        .ifPresent(product -> log.debug("Make-X option {}", product.getIndex()));

InteractionResult result = ProductionActions.chooseOption("Shortbow");
```

Quest and diary progress reads mirror requirement helpers:

```java theme={null}
QuestProgressApi.isComplete(Quest.COOKS_ASSISTANT);

DiaryProgressApi.progress(DiaryRegion.LUMBRIDGE, DiaryTier.EASY)
        .filter(DiaryProgress::isComplete)
        .ifPresent(progress -> log.debug("Lumbridge easy complete"));
```

`DiaryProgressApi` exposes only explicitly mapped varbit and varplayer-backed tasks. Unknown diary coverage is absent rather than guessed.

## The rules reviewers enforce

1. Do not sleep. Let the tick dispatcher drive progression.
2. Keep dialogue handlers stateless: click what is visible, otherwise do nothing.
3. Use an intentional spatial selector: `nearestByPath()` for reachable targets, `walkable().nearestToPoint(anchor)` for a work area. Never `first()` as a nearest-target policy.
4. Gate full automation on the break handler; pause actions during planned or active breaks.
5. Inspect `InteractionResult` at the decision point. Do not compare messages and do not treat dispatch as completion.
6. Resolve IDs from `gameval.ItemID`, `IdMapRegistry`, and `WidgetCatalog`. Never hardcode identifiers.
7. Route every write through `Api.actions.*`. Do not call raw packet queues directly.
