Skip to main content
Formulas describe what work looks like. Orders describe when it should happen. An order pairs a trigger with an action — a formula or a shell script. The orchestrator (gc start launches it) wakes every 30 seconds — a tick — and evaluates each order’s trigger. When a trigger opens, the order fires, no human dispatch needed. For a v2 formula, firing hands the whole job to the orchestrator: it decomposes the formula into a graph of beads, fans the ready steps out across the pool’s agents, gates each step on its dependencies, retries failures, and drives the graph to completion outside any session — now on a schedule or in response to events instead of a person typing gc sling. This page assumes my-city is running with agents and formulas configured (as built up through Tutorial 06).

A simple order

Orders live in an orders/ directory at the top level of your city, alongside formulas/ and agents/ — the local (root) pack’s own contents, the same structure an imported pack provides. An order imported from a shared pack reads exactly like a local one. See primitives for how packs, formulas, agents, and orders relate.
Here’s a minimal order that dispatches the pancakes formula from Tutorial 05 every five minutes:
The pool field tells the orchestrator where to send the work. A pool is a named group of agents that share a work queue (you saw the bundled dolt.dog pool in Tutorial 01); a single agent’s name works as a pool target too, so the examples here route to the worker agent from Tutorial 05. When the order fires, the orchestrator runs the formula and writes gc.routed_to=worker on the resulting work beads — the marker that the pool’s bd ready query and the supervisor’s scale_check both read. Any agent in the pool can then pick the work up. The order name comes from the file basename (pancakes-check.toml → pancakes-check), not from anything in the TOML. A scaled-from-zero pool only wakes if the order produces work that is immediately Ready — beads with no open blockers. v2 formulas (anything declaring [requires] formula_compiler = ">=2.0.0", see Choosing a Compiler Contract) guarantee this: their step beads route to the pool and any step with no open blockers is Ready right away. Prefer v2 for new formulas.
A v1 formula whose root materializes as a container bead puts no immediately-Ready work on the pool — bd ready filters the container out, and the dispatcher logs a warning telling you to convert the formula to v2 before routing it to a pool. After upgrading from an older dispatcher, drain any already-dispatched container work with gc order sweep-tracking --include-wisps <order>, or let a min-floor worker clear it, before expecting the order to fire again from a scale-from-zero pool.
Drop a new order file into orders/ and it shows up within a minute — the orchestrator rescans the order set as it ticks, at most once per minute. No restart needed.

Inspecting orders

Three commands show you what the orchestrator sees: which orders exist, what their triggers look like, and whether any are due. gc order list shows every enabled order, whether or not it has ever fired:
Your output will also include built-in housekeeping orders that ship with the bundled core pack (beads-health, gate-sweep, jsonl-export, reaper, etc.). Leave them alone. The TARGET column is the pool the order routes to (the field is still pool in the TOML). To see the full definition:
To check which orders are due right now:

Running an order manually

Any order can be triggered by hand, bypassing its trigger:
The bead ID (mc-2xz) is the dispatched work, stamped gc.routed_to=worker so the pool sees it (wisp is the CLI’s label for that v1 materialization). Exec orders print Order "<name>" executed (exec). Use this to test a new order or kick off work that’s almost due anyway.

Trigger types

The trigger controls when an order fires. Each type reads one extra field from [order]:
Notes per trigger:
  • cooldown — fires immediately on the first tick if it has never run, then waits interval since the last run. Drifts: a 3:02 run means the next is at 3:07.
  • cron — a 5-field expression (minute, hour, day-of-month, month, day-of-week) supporting *, integers, comma lists (1,15), ranges (9-17), and steps on either (*/15, 5-59/10). A step counts from the start of its range, so */2 on day-of-month means the 1st, 3rd, 5th. Every field is bounds-checked at order discovery: an out-of-range or unparseable schedule is a load error, not an order that quietly never fires. Unlike cooldown it hits the same wall-clock times every day. Fires at most once per minute.
  • condition — the orchestrator runs sh -c "<check>" each tick, bounded by the order’s check_timeout (a positive Go duration, default 10s). This is separate from timeout, which bounds the dispatched formula/exec rather than the check. Use it for external state: check a file, ping an endpoint, query a database. The check runs synchronously, so a slow one delays the rest of the tick — keep it fast, or raise check_timeout when a check must query a slow store (a check killed by its deadline never proves its condition, so the order would otherwise silently never fire).
  • event — fires whenever the named event appears on the bus. Cursor-based tracking advances a sequence marker per firing, so the same event isn’t processed twice. Delivery is at least once, though: one bead close can reach the bus as two bead.closed events, one from the process that closed it and one from the orchestrator when it notices the close first. Make an event order’s action safe to repeat for the same bead.
  • manual — shows up in gc order list and gc order check (always DUE no, reason manual trigger — use gc order run), but auto-fires never.
Cooldown vs cron timing on two timelines: cooldown fires a fixed interval after each run ends, so its wall-clock times drift later as runs take longer; cron fires at fixed clock times no matter how long runs take.
If no orchestrator tick landed during a scheduled minute (the orchestrator was busy or down), the next tick fires the missed occurrence once — gc order check shows the reason cron: caught up missed occurrence. Orders that have never run don’t backfill; they fire only on an exact match.

Formula orders vs. exec orders

An order’s action is either a formula or a shell script. An exec order runs the script on the orchestrator — no agent, no LLM, no work beads — which is the right choice for purely mechanical work: pruning branches, running linters, checking disk usage.
The rules:
  • Every order has either formula or exec, never both.
  • Exec orders can’t have a pool — there’s no agent pipeline to route to.
  • The script receives ORDER_DIR (the directory containing the order file) in its environment; pack-sourced orders also get PACK_DIR.
  • An [order.env] table exports extra environment variables into the script — handy for tuning thresholds without editing it. env is exec-only.
  • Default timeout is 30s for formula orders, 300s for exec orders.

Timeouts

Each order can set a timeout:
For formula orders, the timeout covers only the initial dispatch — compiling the formula, materializing its work beads, and routing them to the pool. Once dispatched, the orchestrator drives a v2 formula’s steps to completion at each agent’s own pace; the order timeout never kills work mid-flight. For exec orders, the timeout covers the full script execution — overrun, and the process is killed. A global cap in city.toml:
The effective timeout is the lesser of the per-order timeout and the global cap.

Order scope

When a pack is imported into more than one rig, its orders instantiate once per rig by default — usually right for an order that acts on a single rig’s work. But a sweep or health probe that already iterates over every rig internally would then run redundantly, once per rig. Mark such an order city-scoped so it registers exactly once, no matter how many rigs import the pack:
A scope = "city" order appears once in gc order list with no rig qualifier; rig-scoped orders appear once per importing rig (mirroring scope on [[named_session]]). A city-scoped order keeps the formula layer of the pack it was scanned from, so its formula must resolve from that pack rather than from any one rig’s local orders/ directory.

Disabling and skipping orders

Set enabled = false in an order’s own definition to drop it from scanning entirely — it won’t appear in gc order list or get evaluated:
Or skip orders by name in city.toml without editing their files — handy when a pack provides orders you don’t want running:

Overrides

When a pack’s order is almost right, tweak it in city.toml instead of copying the file:
Overrides match by order name and can change enabled, trigger, interval, schedule, check, on, pool, timeout, idempotent, and env. An override targeting a nonexistent order is an error, not a silent no-op — gc order commands fail; gc start logs the error and continues with the unmatched override skipped.
Many orders expand at scan time into one instance per rig (anything in a rig’s orders/ directory or a pack imported into a rig). When the same order appears city-wide AND per-rig, the override must say which instance it targets via rig:
A rigless override against a name that exists ONLY as per-rig copies is an error; the message names the rigs so you know what to type. The literal "*" is reserved as the wildcard token and may not be a real rig name.

Order history

Every time an order fires, Gas City creates a tracking bead labeled with the order name. You can query the history:
The tracking bead is created synchronously before the dispatch goroutine launches — which is what keeps the cooldown trigger from re-firing on the very next tick. The trigger checks for recent tracking beads when deciding if the order is due.

Duplicate prevention

Before dispatching, the orchestrator checks whether the order already has open (non-closed) work. If it does, the order is skipped even when the trigger says it’s due — so a still-running pancakes batch doesn’t get a second one piled on top. The open-work check runs against the store with a bounded timeout; if the store is so contended that the check times out, the order is skipped — it fails closed. Orders whose dispatch is safe to repeat (sweeps and feeders where a duplicate run is a no-op) can set idempotent = true to fail open instead: on a gate timeout they dispatch anyway rather than starve. A third option exists for orders that consume no bead work at all — pure probes and sweeps that track nothing. no_work_gate = true skips the open-work gate entirely; the dispatcher never issues the store read, so a slow store cannot time it out and skip the order. The canonical case is provider-health-probe, a cooldown probe whose only job is to refresh a health cache; under store contention its gate timed out every cycle and the cache went stale (#2893). no_work_gate and idempotent are distinct: idempotent enters the gate but fails open on timeout, while no_work_gate never enters it. Use no_work_gate only when the order genuinely tracks no beads — it disables single-flight protection, so the order must be self-idempotent or interval-bounded to guard against overlapping re-runs. A separate flag governs capacity rather than duplicates. Each tick dispatches only a bounded number of orders, so when general dispatch capacity is saturated the core fleet-health orders can be crowded out by ordinary work. Setting reserved_dispatch = true declares an order eligible for a small reserved lane that keeps those health orders running under saturation. Declaring it in TOML is the only way to grant that eligibility — the orchestrator never name-matches specific orders — and every order defaults opted out. Today the flag is declaration-only: the bundled health orders carry it, but no dispatcher consumes it yet, and it changes nothing about gate, suspension, or single-flight semantics — a reserved order is still skipped when its city or rig is suspended, still subject to the open-work gate, and still single-flighted.

Rig-scoped orders

When a pack is applied to a rig, that pack’s orders come along and run scoped to that rig. Say a dev-ops pack includes a test-suite order:
And your city applies that pack to two rigs:
Now the city has the same order running independently for each rig:
Two rows, same name, one per importing rig. As soon as any rig-scoped order exists, gc order list adds a RIG column (city-level orders show -). To act on a specific instance, pass --rig:
These are two independent orders — each copy has its own cooldown timer, tracking beads, and history, distinguished internally by scoped name (test-suite:rig:my-api vs test-suite:rig:my-frontend). The pool target is auto-qualified at dispatch: pool = "worker" becomes gc.routed_to=my-api/worker, routing work to the rig’s own agents rather than the city-level pool.

Order layering

When the same order name exists in both a pack and a local orders/ directory within one scope (the city, or one rig), local wins and replaces the pack definition entirely. So a city-level orders/test-suite.toml with a 1-minute cooldown overrides the dev-ops pack’s 5-minute test-suite — the pack version is ignored. Replacement never crosses scopes, though: a rig’s test-suite doesn’t replace a city order of the same name. As the previous section showed, those coexist as independent scoped instances (test-suite vs test-suite:rig:my-api).

Putting it together

Two orders: a frequent lint check (exec, no agent) and weekly release notes (formula, dispatched to the worker agent from Tutorial 05). The pieces are the two order files and the formula they dispatch.
The lint check fires immediately (never run + cooldown = due), then every 30 seconds. The release notes fire Monday at 9 AM: the orchestrator compiles the three-step formula into a graph, makes gather Ready, and routes step beads to the worker pool; summarize unblocks only when gather closes, post only after summarize. The orchestrator drives the sequence to completion — no one types gc sling, no single session babysits the run. That’s the whole idea. An order binds a trigger to an action. For a v2 formula, the action is the full orchestration the orchestrator drives outside any session — now gated by time, schedule, condition, or event. For an exec script, it’s a mechanical task on the same tick. Either way the orchestrator evaluates the trigger every tick and does the work for you.

What’s next

This is the last tutorial in the series. From here, the reference pages and guides go deeper:
  • Formula spec (v2) — the full formula file format, including the v2 runtime constructs your orders dispatch
  • Understanding packs — how formulas, orders, agents, and prompts travel together as importable packs
Last modified on October 7, 2026