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 anorders/ 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.
pancakes formula from
Tutorial 05 every five minutes:
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.
v1 formulas don't wake a scaled-from-zero pool
v1 formulas don't wake a scaled-from-zero pool
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.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:
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:
Running an order manually
Any order can be triggered by hand, bypassing its trigger: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]:
cooldown— fires immediately on the first tick if it has never run, then waitsintervalsince 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*/2on 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 runssh -c "<check>"each tick, bounded by the order’scheck_timeout(a positive Go duration, default10s). This is separate fromtimeout, 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 raisecheck_timeoutwhen 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 twobead.closedevents, 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 ingc order listandgc order check(always DUEno, reasonmanual trigger — use gc order run), but auto-fires never.
Cron catch-up after a missed minute
Cron catch-up after a missed minute
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.- Every order has either
formulaorexec, 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 getPACK_DIR. - An
[order.env]table exports extra environment variables into the script — handy for tuning thresholds without editing it.envis exec-only. - Default timeout is 30s for formula orders, 300s for exec orders.
Timeouts
Each order can set a timeout:city.toml:
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: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
Setenabled = false in an order’s own definition to drop it from scanning
entirely — it won’t appear in gc order list or get evaluated:
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 incity.toml instead of copying
the file:
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.
Disambiguating overrides when an order exists both city-wide and per-rig
Disambiguating overrides when an order exists both city-wide and per-rig
Many orders expand at scan time into one instance per rig (anything in a rig’s
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
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:"*"
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: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 setidempotent = 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 adev-ops pack includes a test-suite order:
gc order list adds a RIG column (city-level orders show -). To act
on a specific instance, pass --rig:
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 localorders/ 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 theworker agent from
Tutorial 05). The pieces are the two order files and
the formula they dispatch.
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