gc formula show), apply one method across many runs with different inputs
(--var), and catch when a run has drifted from the formula it came from (gc formula version-check).
A bead is one unit of work; a
convoy is a graph of related work; a formula is the
reusable method that produces and orders it. Applying a formula materializes its
steps as beads, and from that moment the work is independent of both the formula
file and any agent session: sessions crash and recycle, but the work persists,
so whoever picks it up next finds the same state.
Choosing a Compiler Contract
Both contracts are live and supported. They are peers, not a version ladder — each makes a different thing the engine.steps, needs, children, condition, loop, vars,
extends) mean the same in both contracts. Graph-only constructs (check,
retry, drain, on_complete, tally, and reserved gc.* step metadata)
require the v2 declaration; compiling without it fails with requires: formulas that use graph-only constructs must declare [requires] formula_compiler = ">=2.0.0" or the deprecated contract = "graph.v2" explicitly.
Two v1-only edges remain, neither a reason to start on v1:
gc convergeaccepts only v1 formulas (it rejects v2 until it has an explicit input convoy target). For iterate-until-it-passes behavior, use a v2 check loop instead.- Container dependencies have a v2 gap. Under v1 a step that
needsa parent waits for all of that parent’s children; the v2 compiler creates no parent-child edges yet, so the dependency gates only on the parent step (#3451). Until that lands, list the children you depend on explicitly inneeds.
Cook, Sling, or Order — and What Lands in the Store
Two more decisions follow the contract: the verb (how the instance gets created and routed) and the outcome (what lands in the store, which follows from the contract). Three verbs create formula instances:- Cook creates without routing.
gc formula cook <name>compiles the formula, writes its beads into the current scope’s store, and stops. Nothing wakes up. Cook to inspect the beads first, route the work yourself, or graft a sub-DAG onto existing work with--attach <bead-id>.--attachis limited on a split city — one that serves the graph class from its own[storage]binding. See Grafting on a split city. - Sling creates and routes.
gc sling <target> <name> --formulacooks and routes in one motion: a v2 formula starts a workflow, a v1 formula starts a single-bead run (a wisp), both routed to the target. The one-shot dispatch verb. - Orders are scheduled dispatch. An order names a formula (or a shell command — never both) and a trigger; the orchestrator instantiates and routes the formula to the order’s pool each time the trigger fires. The schedule runs the verb for you.
Visibility, routing, and cleanup tradeoffs
Visibility, routing, and cleanup tradeoffs
- Visibility. Materialized steps are real beads you can list, show, and watch move through statuses — a per-step audit trail. A single-bead run keeps the store lean but gives one bead and no step-level record.
- Routing. v2 steps each route to a different agent or pool; a v1 run is worked end-to-end by the one agent it was slung to. A pool wakes only for Ready-visible work, so slinging a v1 run at a pool is refused outright — convert to v2 first.
- Cleanup. A single-bead run is ephemeral by design — fire-and-forget activity you need no record of. v1-with-steps and v2 workflows leave a per-step record. The core pack’s cleanup order tidies all three: it reaps stale ephemeral runs and purges closed step records, with cleanup edges covering v2 workflows too.
--rig flag, else the enclosing
rig directory, else the city), and sling refuses a cross-store route with
refusing cross-store route, telling you to re-file the bead or pick a
reachable target. City-scoped agents are the exception: they are cross-store
eligible and may serve work in any store.
Major Use Cases
The patterns below cover most of what formulas get used for. Each shows the minimal shape, what happens at runtime, and where the normative detail lives, and points at the formula in the gascity pack that uses the pattern in production.Note: the shipped pack predates the current canon
Note: the shipped pack predates the current canon
Every pack formula opts into v2 with the deprecated top-level
contract = "graph.v2" key (not the [requires] table) and is named
<name>.formula.toml (not <name>.toml). Both spellings still parse —
gc doctor warns about the contract key — and every fence below is adapted to
today’s canon. Migrating the pack is tracked in
#3462.The Whole Job, End To End
Most real formulas are a pipeline of patterns, not a single one. The clearest way to see what a formula is for is to follow one body of work all the way through.build-* chain wires them together with
extends into one composed method, not one
giant file.
Multi-Step Feature Workflows
You have one unit of work with ordered phases — design it, build it, review it, ship it — and you want the phases tracked and gated instead of trusted to an agent’s memory.needs gates readiness so
implement stays invisible until design closes. You declare the ordering
and the runtime runs whatever is ready — work flows in dependency-ordered
waves, the same mechanism that lets a
drain run a whole convoy’s ready
members in parallel. The appended finalize step closes the workflow when the
last step completes. The same file without [requires] compiles under v1 into
a molecule instead.
In the wild: the build pipeline composes with extends
In the wild: the build pipeline composes with extends
Real multi-step builds rarely live in one file. The pack’s build pipeline is a
chain of
extends bases — a prepare → requirements → plan → plan-review → decompose → implement → review → finalize → publish flow assembled across
build-base
and the build-from-*-base family — and the entries you dispatch are thin
wrappers over those bases.Composition uses two extends rules: a child’s steps are appended to the
parent’s, but a child step that reuses a parent step’s id overrides it in
place, keeping its position. That rule is how a base declares a skeleton and a
descendant splices new steps into the middle — the descendant redeclares an
inherited step id with a new needs list, and the inserted steps land before
it.The cataloged entrypoint adds almost nothing. The pack’s
build-from-convoy
is 21 lines with no [[steps]] — it extends the base and supplies the catalog
name plus methodology defaults:gc formula show build-from-convoy resolves the extends chain and prints the
base’s full step graph under this name. Build new methodology variants the same
way: extend the matching base and override defaults or routes, rather than
copying the step graph.Parameterized Templates
You want one definition to serve many runs — same workflow, different feature, environment, or target. Declare variables, constrain the dangerous ones, and supply values at instantiation.{{placeholders}} substitute into titles, descriptions, notes, assignee, and
metadata values. required and enum are enforced at instantiation, so a
missing or misspelled env fails before any bead exists. Every interactive
path takes repeatable --var — gc formula show, gc sling --formula, gc formula cook, and gc converge create.
In the wild: vars keep build bases swappable
In the wild: vars keep build bases swappable
Variables are how the pack’s build bases stay swappable without forking the
graph. They flow down the
extends chain and substitute into both routing
metadata and child-formula names. The convoy base (build-from-convoy-base)
routes its drain step to "gc.run_target" = "{{implementation_target}}", so
overriding that one var re-points every drained unit at a different worker
role; its {{drain_policy}} var selects which drain step survives compilation
(next section); and the review bases’ {{code_review_formula}} var (default
"review") names which formula the review stage dispatches, so a methodology
pack can swap the whole reviewer by setting a default.The lesson for your own templates: put the swappable decisions — target roles,
sub-formula names, policy switches — in vars on the base, and let descendants
override the defaults instead of editing steps.Orders are the exception: order TOML has no variable mechanism and
gc order run has no --var flag
(#1813), so a formula
with required variables cannot be dispatched by an order. Give every
variable a default if the formula must run on a schedule.Fan-Out Over a Runtime-Discovered Set
You do not know the work items until runtime — a convoy holds however many review requests, failing tests, or implementation beads exist right now, and you want one workflow instance per item, running in parallel.drain is the
canonical v2 fan-out for this, and it is the pack’s single load-bearing
parallelism pattern: every build entrypoint drains an implementation convoy
into per-member units.
build-from-convoy-base,
declares two drain steps guarded by a policy variable and lets compilation
pick one:
gc sling gc.run-operator <convoy-id> --on build-from-convoy); an untargeted
run fails with requires a target convoy. Core injects the convoy as the
reserved convoy_id target, so the formula never declares it. At runtime the
orchestrator scatters the input convoy into one-member unit convoys and runs the
item formula (itself a v2 formula) once per unit; member_access = "exclusive"
reserves each member so no second drain can claim it.
In the wild: the policy fork and swappable item formula
In the wild: the policy fork and swappable item formula
Two details make this the pack’s workhorse. First, the policy fork: both
drain steps
needs-feed review, but condition filters them at compile
time, so exactly one survives. gc formula show build-from-convoy prints
implement (the context = "separate" branch, where every unit gets its own
git worktree and they all run in parallel); --var drain_policy=same-session
prints implement-same-session instead (context = "shared",
item.single_lane = true, running units one at a time and skipping the rest
after the first failure via on_item_failure). The surviving step’s name flows
into review’s needs automatically.Second, the item formula is swappable — the real base points separate at
do-work
(a checked prepare-worktree → implement → close-source-anchor flow) and
shared at do-work-item, both overridable by descendants.on_complete also fans out, but over a collection in a step’s structured
output rather than over convoy members, and tally aggregates the results
(#2947). The whole
gascity pack is pure-drain — zero on_complete, zero tally. Prefer drain
when the set is convoy members; reach for on_complete only when the set
exists solely in a step’s output. (Raw gc.output_json_required fan-out is
deprecated; gc lint warns gc.output_json is deprecated; use drain in v2 formulas (see: engdocs/drain-fanout.md).)
See drain and
on-complete and tally in
the v2 spec.
Multi-Lane Review Loops
You want several independent verdicts on the same work — acceptance, test evidence, simplicity — and you want it to keep iterating until the combined verdict says it is done. The pack expresses this not as a one-shot vote but as a loop: review lanes fan out, a synthesizer fans them in, a fix step applies the findings, and the whole subtree repeats until a verdict check passes. The real example isbuild-basic-review,
an expansion formula whose loop core is a [[template]] step carrying a
[template.check] and a set of [[template.children]]:
{target} and {implementation_target} are
expansion-template placeholders, distinct from {{var}} substitution; when the
host expands the template, {target} is rewritten to the host step’s id. gc formula show code-review compiles it into an iteration.1 scope: the three
review children run in parallel (no needs between them), each on a different
reviewer role, fan in to synthesize-review, then apply-review-findings
makes the smallest fixes and records a verdict in bead metadata. The orchestrator
— never an agent — then runs implementation-review-approved.sh; while the
verdict is iterate and budget remains (max_attempts = 6), it re-spawns the
entire lanes-synthesize-fix subtree as the next iteration. The verdict
travels as bead metadata; no judgment lives in Go.
For quorum voting — verdicts reduced by majority, unanimous, or
any-pass — use on_complete plus tally (under
Fan-Out); the gascity pack does not,
preferring the synthesize-and-recheck loop above.
See check and
loops in the v2 spec.
Self-Checking Work And Transient Hardening
Two different failure modes, two different constructs — mutually exclusive on the same step.check is for work you can verify: the step is done when your script says so,
not when the agent says so.
The pack’s
fix-loop-base
is the production version — it plans fixes, applies them, re-reviews, and gates
the re-review with a [steps.check] that runs an artifact validator:
re-review closes, the orchestrator runs the script: pass closes the
step; fail with budget left spawns the next iteration; exhaustion closes the
step failed and blocks downstream work. The validator resolves the artifact
from the keys named in gc.build.artifact_path_keys and checks it against the
gc.build.artifact_schema, so “done” means the schema validator agrees, not
the reviewer. Note the layering: this [steps.check] is the bounded,
orchestrator-driven inner loop (max_attempts); the {{max_iterations}} var
bounds an outer loop that callers drive by re-dispatching the whole formula
on a still-failing verdict — judgment in the prompt, iteration in the config.
retry is for steps that fail for boring reasons — provider hiccups,
timeouts — where re-running is the fix:
hard_fail (the default) closes the step as
failed; soft_fail closes it as passed with
gc.final_disposition=soft_fail so downstream work continues with degraded
coverage — the right choice for an optional reviewer lane whose absence
should not block the build.
See check and
retry in the v2 spec.
Planning Reviews And Decomposition
Before a build implements anything, two upstream stages do judgment work: someone reviews the plan, and someone shreds the approved plan into the beads that the drain later parallelizes. The pack models both as ordinary steps whose authority lives in the prompt and whose artifacts are schema-checked — not in Go. Plan review by variable, not by code. The pack’splanning-base
makes the review authority a variable. A single plan-review step routes to
a reviewer role, and a review_mode var tells the prompt how much authority
it has:
plan-review
reads review_mode and decides what to do — report records findings,
agent also produces a fix handoff, interactive applies safe fixes
directly. The judgment is a sentence in the prompt, not a branch in code. For a
heavier plan review that loops review lanes until approved, the pack reuses the
expansion-plus-[template.check] machinery from
Multi-Lane Review Loops — see
github-issue-fix-design-review-work.
Decomposition shreds a plan into a convoy — done by an agent, not a formula
construct. The
build-from-decompose-base
decompose step routes to a decomposer role whose prompt reads the approved
plan, creates an implementation convoy, and stamps the convoy id on the
workflow root so the downstream drain can find it. The formula contributes a
bounded validation loop around that act:
[steps.check] validates the decomposition artifact against its schema
with bounded repair, exactly as the
self-checking pattern does. The
smaller decomposition-base
is the swappable methodology contract, named through the
decomposition_formula var so a build can swap the shredder without touching
the pipeline. The convoy this step produces is the runtime-discovered set the
drain fans out over — decomposition and fan-out are two ends of the same
pipeline.
See check and
variables in the v2 spec.
Scheduled And Maintenance Work Via Orders
Recurring work — digests, sweeps, health checks — should not depend on anyone remembering to sling it. An order binds a formula to a trigger:exec shell command, never both —
deterministic maintenance belongs in exec, judgment work in a formula.
Triggers are cooldown, cron, condition, event, or manual. When the
trigger fires, the orchestrator instantiates the formula and routes it to the
pool. As with sling, a pool wakes only for Ready-visible roots, so order
formulas routed to pools should be v2 (the dispatcher warns otherwise). Test
any order with gc order run <name>, which bypasses the trigger.
See the orders tutorial for triggers, layering, and
overrides.
Choosing The Shape: A Recap
The two frameworks above compose. Find your situation, read across:Grafting On A Split City
A split city serves the graph coordination class from its own[storage]
binding: workflow roots, step beads and control beads live in a second
database, and ordinary work stays in the work ledger. Most of
gc formula cook --attach is not supported yet there, in city and rig
scope alike, and refuses rather than serving.
The reason is that a graft is graph class whatever the formula’s version.
Every bead an attach materializes carries gc.root_bead_id, which is what
classifies a bead as graph — so the sub-DAG belongs in the binding while the
blocking dependency belongs beside the attach bead, and a split city cannot
have both ends in one store.
A rig scope —
--rig, the GC_RIG the orchestrator sets on every rig
agent, or a cwd inside a rig — follows the same rule. Every rig’s graph beads
live in the binding, recorded with the rig as their scope, so a rig’s own
store holds only work. The exception is an older tree: one the rig created
before its graph beads moved to the binding stays in the rig’s own store, and
a v1 graft onto one of its beads extends it there.
Single-store cities — every city that authors no [storage] section — are
unaffected: --attach behaves exactly as it always has, on both contracts.
Lifting the refusals needs a cross-class membership edge, tracked as
ga-2orlf. If an earlier cook already stranded beads in a split city’s work
store, gc storage status names them and
gc storage recover-stranded --from-work --fleet-stopped copies them into
the binding.
Convergence Loops
Some work is not a pipeline but a loop: draft, evaluate, refine, repeat until good enough. Express it as a v2 check loop —[steps.check] re-runs the
work until a verification script passes (see
Self-Checking Work) — which
keeps the loop inside the formula where the orchestrator drives it.
If you need gc converge (v1 only)
If you need gc converge (v1 only)
The dedicated
gc converge command predates the v2 runtime and accepts only v1
formulas, with no convergence-specific formula keys:gc converge create --formula refine-doc --target worker --evaluate-prompt "..."
creates the loop, bounded by --max-iterations (default 5); each iteration
cooks the formula as a convergence wisp with your --var values plus the
evaluate prompt injected as the evaluate_prompt variable, and a gate —
manual approval or a condition script — decides whether to iterate again or
stop. gc converge rejects v2 formulas until it gains an explicit input
convoy target, so reach for it only when you specifically want its
gate-and-evaluate machinery; otherwise prefer the v2 check loop above.See conformance and compatibility
in the v1 spec.Where Next
- The primitives — Formula alongside Agent, Bead, Rig, Pack, and Event.
- Tutorial 05: Formulas — write, inspect, and dispatch your first formulas hands-on.
- Formula spec — v2 and v1 — the normative format, compilation, and runtime rules.
- Tutorial 07: Orders — scheduled dispatch in depth.
- The gascity pack
— real formulas to read: the
build-*chain (extendscomposition),do-work(the drain item contract),fix-loop-baseandbuild-basic-review(check loops), andplanning-base/decomposition-base(review and decomposition).