Skip to main content
This document specifies the v1 formula contract: the file format a formula author writes, how the v1 compiler turns it into a molecule of beads, and what happens to the molecule after instantiation. It is self-contained: the authoring surface shared with formulas v2 is specified here in full. v1 and v2 are peer contracts; both are supported. v1 is the default: a formula that declares no compiler requirement compiles under v1. The v2 contract is specified separately in the Formula Specification — v2. Section 5 specifies the v1-side behaviors v2 has not yet absorbed: the gc converge command accepts only v1 formulas, and v1 container-dependency semantics have a tracked v2 gap (#3451). The key words “must”, “must not”, “required”, “shall”, “shall not”, “should”, “should not”, and “may” are to be interpreted as normative requirements unless the paragraph is explicitly marked as non-normative.

0. Concept And Data Model

0.1. Concept

A formula is a TOML file specifying how work should be carried out — its steps, their ordering and dependencies, and the control flow around them. A formula is not the work itself (a bead is a unit of work) nor a grouping of work (a convoy is a graph of related work); it is the reusable method that produces work when applied. The method is defined independently of how its work is stored; the data model below is the current implementation of that storage, not part of the definition of a formula.

0.2. Data Model

Compilation produces a recipe: a flattened, validated list of steps and dependency edges. Instantiation (gc formula cook, gc sling --formula, or the Go API molecule.Cook / molecule.CookOn / molecule.Attach in internal/molecule) materializes the recipe into the bead store. Under the v1 contract the materialized shape is a molecule: a parent-child tree of beads.
A phase = "vapor" formula without pour materializes a wisp instead: a single root bead (type task, gc.kind = "wisp") with no step beads (section 2). Vapor is a v1-era materialization shortcut from when bead writes were expensive; it remains specified for compatibility, but new formulas should use the v2 contract rather than reasoning in phases. v1 has no runtime engine. Conditions and loops resolve at cook time; after instantiation the molecule is inert data. No orchestrator component advances it — agents work the molecule through their hooks, inside their own sessions, and the bead store’s dependency edges sequence the steps. The orchestrator’s control dispatcher executes only control beads, which v1 compilation never emits. The execution model is the structural difference from v2: A minimal v1 formula — note the absence of any contract declaration:

1. File Format

This section specifies the full authoring surface. The file format is shared with formulas v2; every construct below parses under both contracts. Constructs marked v2-only (check, retry, drain, on_complete, timeout, and reserved gc.* step metadata) require the explicit v2 declaration — using them in a v1 formula must fail compilation (section 5).

1.1. File Naming And Layers

Formula files live in formulas/ directories: Formula directories are collected into layers, ordered lowest to highest priority: Resolution must be last-wins across layers: the highest-priority layer containing a formula name wins. Within a single layer, <name>.toml beats <name>.formula.toml, which beats <name>.formula.json. At city start, init, rig add, and supervisor start, the resolver symlinks each winning file into <scope>/.beads/formulas/ under both <name>.toml and the deprecated <name>.formula.toml alias. Real (non-symlink) files already present in .beads/formulas/ are never overwritten. City-level [formulas].dir is not valid configuration. Authoring it is a hard config error:
and the gc doctor check v2-formulas-dir reports any remaining declaration as an error.
Compatibility: Builds predating the shared last-wins resolver (issue #2027, fixed by #2028) resolved gc formula show/cook/sling first-wins, letting imported pack formulas shadow same-name city overrides. If an override does not take effect, inspect gc formula show <name> against the layer you expect to win.

1.2. Top-Level Keys

Unknown top-level keys are silently ignored, with one exception: unknown keys inside [requires] are hard errors (section 5).

1.3. Steps

Each [[steps]] entry becomes one bead in the instantiated recipe. Rows marked v2-only require the explicit v2 declaration; a v1 formula that uses them must fail to compile with the error quoted below the table. Their semantics are specified in the v2 specification, section 3. Compiling a v1 formula that uses any v2-only construct must fail with:
Unknown step keys are silently ignored. A typo like dependson produces no diagnostic — the dependency simply vanishes.
Container dependencies. v1 compiles containment: every step is linked to its enclosing container — the molecule root, or the parent step for children — by a parent-child edge, and a step with children is promoted to issue type epic (section 2). Because v1 containers close only when their members are done (the molecule root is auto-closed only when every transitive descendant is terminal, and epic closure in the bead store follows the epic’s children), a blocking dependency on a container waits for the container’s entire subtree, not just the container bead. This is the v1 container dependency semantic. The v2 compiler creates no parent-child edges, so the same dependency under v2 gates only on the named step bead.
The bead store restricts blocking edges across the task/epic boundary: materialization fails with tasks can only block other tasks, not epics (or its inverse) when exactly one endpoint is an epic. Since a step with children always compiles to an epic, a default-typed step cannot needs a parent that has children — the formula validates but gc formula cook fails when the edge is written. Make the dependent a container too (give it children), or depend on the parent’s terminal child instead.

1.4. Variables

Declare variables in a top-level [vars] table. Two forms exist: a string shorthand that sets only the default, and a table form with validation.
Table-form fields: {{key}} placeholders substitute into descriptions, titles, notes, assignee, and metadata values. Values are supplied as key=value pairs at instantiation:
Here worker is a pack-supplied agent name, not a platform built-in; targets in these examples are illustrative names a pack declares. Injected names. v1 reserves no variable names; the v2 reserved-variable rules (convoy_id, bead_id) do not apply. On a targeted invocation (gc sling <target> <bead-id> --on <formula>), the router injects issue — the target bead’s ID — plus the routing variables rig_name, binding_name, and binding_prefix, and an automatic base_branch / target_branch when the formula references them. Precedence, highest first: explicit --var > rig formula_vars > routing-injected values > formula-level defaults. Under v2 the issue injection is replaced by the reserved {{convoy_id}} derivation, and issue survives there only as a deprecated compat alias.

1.5. Conditions

A step condition is a compile-time include/exclude filter evaluated during compilation, before any beads exist. The grammar is: Falsy values are: empty string, false, 0, no, off. Everything else is truthy. Excluded steps are removed from the recipe along with their dependency edges. This grammar applies only to step condition. Loop until conditions use the runtime condition evaluator’s grammar instead (section 1.6).

1.6. Loops

A step with a [steps.loop] table becomes an iteration container that expands its body steps in place — the container step itself is replaced by the expanded iterations. Exactly one of count, until, or range is required, and body must be non-empty. A range loop (the same shape works with count):
gc formula show hanoi renders the chained iterations:
An until loop expands a single iteration and records the condition and max budget as a loop: label on the first body step:
Two caveats. First, the re-run never happens: the until label is written but nothing consumes it — neither the v1 cook path nor the v2 control dispatcher — so an until loop runs exactly one iteration (section 4). v1 has no runtime re-execution mechanism at all; orchestrator-driven re-execution requires the v2 check construct (v2 specification, section 3.1). Second, until does not use the {{var}} == value step-condition syntax: until = "{{ready}} == yes" fails with unrecognized condition format. The grammar is the runtime condition evaluator’s: probe.status == 'complete', step.output.field == value, children(x).all(status == 'complete'), steps.complete >= 3.

1.7. Composition And Inheritance

extends composes a child formula from one or more parents:
  • Child steps replace parent steps with the same ID whole-step (no field-level merge), preserving the parent’s position; new child steps append.
  • Parent vars are inherited; child declarations override.
  • phase is taken from the child, else the first parent that declares one.
  • pour is monotonic: any ancestor’s pour = true sticks; a child cannot opt out.
  • contract and requires come from the child, else the first parent — and requirement constraints from every parent are still collected and validated as a set (section 5). A v1 child extending a v2 parent inherits the v2 requirement and stops being a v1 formula.
  • Circular extends chains must fail (circular extends detected: a -> b -> a).
Expansion formulas (type = "expansion") declare template steps with {target} / {target.description} placeholders; a step’s expand key inlines an expansion formula in place of the step, with expand_vars overriding variables for that expansion.
The formula resolved through extends drops advice and pointcuts entirely — including the child’s own. And when both parent and child declare compose rules, the merge keeps only bond_points, hooks, expand, and map; branch, gate, and aspects rules from both sides are dropped. Do not rely on full inheritance of composition rules.

1.8. Description Files

Use description_file when a step’s instructions should live in a separate Markdown file instead of inline TOML. Non-asset paths resolve relative to the formula file. Paths using ../assets/... resolve through the same low-to-high formula layer order as the formula itself (section 1.1). A bundled formula can reference:
and a city or higher-priority pack can override only that prose by providing the same asset path next to its formula layer. The formula structure and step IDs remain inherited from the lower-priority pack; only the description file content is shadowed. Other relative or absolute paths that happen to contain an assets segment still use normal formula-relative or absolute resolution. Description file reads use the same configured source as formula reads, so a parser pinned to a git ref also reads committed description file content from that ref. Two behaviors are normative:
  • Files larger than 4096 bytes are not inlined. The step’s description is replaced by a generated pointer that directs the agent to read the file at its resolved path.
  • A v1 formula skips an unresolvable description_file best-effort: the reference is left unconsumed and any inline description is kept, with no diagnostic. (v2 formulas fail fast instead.)

1.9. Validation

A formula must satisfy these structural rules; violating any is a validation error. The rules apply to the shared file format; constructs marked v2-only in section 1.3 are additionally rejected in a v1 formula with the section 1.3 compile error.
  • formula name is required.
  • contract, if set, must be "graph.v2" (contract: invalid value "<value>" (must be graph.v2)).
  • type must be workflow, expansion, or aspect.
  • A var must not combine required = true with a default.
  • Step id is required and globally unique, including children.
  • Step title is required unless expand is set.
  • priority must be 0–4.
  • depends_on / needs entries must reference known step IDs (including children).
  • waits_for must match all-children, any-children, or children-of(step-id) with the referenced step present (waits_for has invalid value "<value>" (must be all-children, any-children, or children-of(step-id))).
  • timeout must be a positive Go duration and requires check — both v2-only; a bare timeout in a v1 formula fails with a message routing convergence gate scripts to convergence.gate_timeout / --gate-timeout.
  • Loops (validated during control-flow expansion): body non-empty; exactly one of count / until / range; max required with until.
The v2-only constructs carry their own shape rules (check / retry / drain / on_complete field constraints); tally was removed and now fails fast if authored. The remaining construct constraints are specified with the constructs in the v2 specification.

2. Compilation

Compilation is a fixed pipeline shared with v2: load → resolve extends → control-flow expansion (loops, branches, gates) → advice → inline expansion → compose expand/map → aspects → condition filtering → standalone expansion → requirement merge and the explicit-declaration check → host requirement validation → recipe. The graph-specific stages (reserved-symbol validation, retry and check transforms, graph control injection) do not apply to a v1 formula: the explicit-declaration check rejects their inputs first. The v1 compiler must emit a molecule tree:
  • Namespaced step IDs. Every step gets the recipe ID <formula>.<step>; nested children extend the path (<formula>.<parent>.<child>). The namespaced ID is stamped on each cooked step bead as gc.step_ref metadata.
  • Parent-child containment. Every step carries a parent-child edge to its container — the molecule root for top-level steps, the enclosing step for children. A step with children is promoted to issue type epic.
  • Blocking edges. needs / depends_on compile to blocks edges; waits_for compiles to a readiness-blocking waits-for edge plus a gate:<value> label on the cooked step (section 4).
  • Gate synthesis. A [steps.gate] table synthesizes a gate bead (recipe ID <formula>.gate-<step>, type gate, title Gate: <type> <id>) that is a child of the step’s container, plus a blocks edge from the gated step to it: the step stays blocked until the gate bead is closed.
  • Root. The recipe root is type molecule, priority 2. Its title is the formula name, or the {{title}} placeholder when a title var is declared; its description is the formula description, or {{desc}} when a desc var is declared.
  • Root-only wisps. When the formula sets phase = "vapor" without pour, or has no steps, the recipe is root-only: the root is type task stamped gc.kind = "wisp", and steps are not materialized as beads — the root bead itself is the work, carrying only the formula’s title and description. Authors of root-only formulas should put the work instructions in the formula description. Setting pour = true forces full materialization (checkpoint recovery) regardless of phase.
  • Step-type coercion at instantiation. Non-root step beads typed task (the default) are stamped type step so Ready() and bd ready skip formula scaffolding — the molecule root is the actionable unit. Other explicit types (bug, epic, …) are preserved.
  • Root stamping. Non-batch instantiation stamps gc.formula_hash (SHA-256 of the raw formula file bytes) and gc.formula_source on the root; gc formula version-check <bead-id> compares the stored hash against the on-disk recipe to detect formula drift since spawn.
Preview. gc formula show <name> renders the compiled recipe. For the minimal formula of section 0 (five authored steps render as five — v1 appends no finalize step):
A step with children renders with an (epic) marker:
gc formula cook pancakes materializes the molecule; the created count is steps + root, and each bead gets an independent store ID:
gc formula cook <name> --attach <bead-id> grafts the compiled recipe under an existing bead as a sub-DAG; the attach target gains a blocking dependency on the sub-DAG root, so it cannot close until the sub-DAG completes. A graft is GRAPH class even for a v1 formula: the attach path stamps gc.root_bead_id on every bead it materializes, which is what makes a bead graph class, so the sub-DAG is graph class regardless of what the same recipe would compile to standalone. On a city that serves the graph class from its own [storage] binding, --attach is therefore served for an attach bead the BINDING owns, in CITY and RIG scope alike; grafting onto a bead in a work store is refused, because the sub-DAG would either be stranded in the work ledger or leave a blocks row naming an id the work store cannot resolve. That includes a RIG scope’s own store (--rig, GC_RIG, or a cwd inside a rig), which holds only work on such a city; launch the formula against a work bead with gc sling <target> <bead> --on <formula> instead. The one graft a rig’s own store still serves is onto a graph bead of an older tree, one the rig created before its graph beads moved to the binding: the graft extends that tree where it lives. Single-store cities are unaffected. See formula-spec-v2 §3 and gc formula cook --help for the full residence table; the cross-class membership edge that lifts the refusal is tracked as ga-2orlf. gc sling <target> <formula> --formula instantiates the formula as an ephemeral wisp and routes the root bead to the target:
A root-only vapor formula previews and cooks to a single bead:

3. Runtime

v1 has no runtime engine. The orchestrator’s control dispatcher executes control beads only, and v1 compilation emits none — after instantiation no orchestrator component advances the molecule.
  • Agents advance work. The molecule is worked by the agent it is slung or hooked to; the bead store’s blocking edges sequence the steps. Neither the molecule container root nor its default-typed step beads surface through Ready() (section 2) — the molecule routes as a unit by being assigned to an agent directly, which is also why scale-from-zero pools cannot wake for stepped molecules (section 5).
  • Targeted invocations. gc sling <target> <bead-id> --on <formula> injects the issue variable and routing variables specified in section 1.4. Formula slings do not create an auto-convoy; convoys, where they exist, track members through non-blocking tracks edges that never gate readiness.
  • Completion. The molecule root is auto-closed by the bd close hook when every transitive descendant is terminal (close reason molecule autoclose: all step children closed). Container dependents unblock at that point (section 1.3).
  • Garbage collection. A core-pack cleanup exec order (cooldown trigger, 30m interval) closes stale wisps whose parents or roots are closed, purges old closed molecule data, and closes TTL-expired beads.

4. Accepted But Inert

This specification is normative for implemented behavior. The constructs in this section are accepted by the parser and compiler but have no runtime consumer in the current release. Authors should not rely on them; they are documented to prevent silent surprise.
  • Until-loop re-execution. Compiling an until loop validates the condition and writes a loop:{"until":...,"max":...} label on the first body step (the loop expander in internal/formula/controlflow.go), but no component — neither the v1 cook path nor the v2 control dispatcher — reads that label. An until loop therefore runs exactly one iteration.
  • Gate type vocabulary. [steps.gate] synthesizes a real gate bead that blocks its step until the gate bead is closed (manually or by an external watcher), but the type values gh:run, gh:pr, timer, human, and mail are doc-comment vocabulary in internal/formula/types.go — the parser never validates them and no bundled watcher acts on them. Zero bundled formulas use gate.
  • waits_for gate modes. waits_for compiles to a readiness-blocking waits-for dependency edge plus a recorded gate mode, but no current component interprets the all-children / any-children distinction. Zero bundled formulas use waits_for.
  • vars.<name>.type. The variable type field (string, int, bool) is parsed into the variable definition (internal/formula/types.go) but never enforced; only required, enum, and pattern are validated at instantiation.

5. Conformance And Compatibility

Contract selection

v1 is the default contract. The host’s formula compiler capability is 2.0.0 when [daemon] formula_v2 is enabled (the default) and 1.0.0 when it is disabled; a formula compiles under v1 whenever no declared constraint rejects capability 1.0.0. formula_compiler is the only [requires] axis. The value must be a semver comparator; violations fail with formula.compiler_requirement_invalid: formula_compiler must be a semver comparator, for example ">=2.0.0", and unknown axes fail with formula.requirement_unknown: unknown formula requirement "<key>"; supported requirements: formula_compiler. [requires] composes through extends and through composed expansion and aspect formulas as a safety constraint: a child inherits every parent requirement and may only add tighter constraints, so a v1 formula that composes in v2-requiring material stops being v1. Non-overlapping constraint sets must fail before any durable work is written (formula.compiler_requirement_conflict). [daemon] formula_v2 interplay. Disabling the host switch lowers the compiler capability to 1.0.0 and makes v2 formulas fail to compile; it must not change the behavior of v1 formulas, which compile identically under either setting.

v2-only constructs are rejected

A v1 formula that uses check, retry, drain, on_complete, or reserved gc.* step metadata (the control and structural gc.kind values, gc.scope_name, gc.scope_role, gc.scope_ref, gc.continuation_group, gc.on_fail) must fail to compile with:
This check runs after expansions and aspects materialize, so composed-in constructs trigger it too. A drain step additionally fails parse-time validation in a v1 formula: <step>.drain: drain steps must declare the formulas v2 contract ([requires] formula_compiler = ">=2.0.0"). The gc doctor check formula-requirements reports, per city and rig formula layer: parse failures, deprecated contract = "graph.v2" opt-ins, v2-only constructs in formulas without an explicit v2 requirement, and host capability mismatches.

Convergence currently requires v1

For iterate-until-verified semantics, prefer a v2 check loop (v2 spec section 3.1); gc converge is the pre-v2 command for this pattern and accepts only v1 formulas. gc converge loops instantiate ordinary v1 formulas — there are no convergence-specific formula keys. Top-level convergence, required_vars, or evaluate_prompt keys in formula TOML are not decoded (unknown keys are silently ignored). The evaluate prompt is supplied at creation time with gc converge create --evaluate-prompt, stored as bead metadata convergence.evaluate_prompt, and injected into the cook as the evaluate_prompt variable. Convergence wisps must reject v2 formulas:

Ready-visibility and pools

A stepped v1 molecule’s container root is not Ready-visible work, so it cannot wake a scale-from-zero pool. gc sling --formula must reject a stepped v1 formula routed to a multi-session target — any agent with a namepool, or whose max_active_sessions is unset or not 1:
Order dispatch applies the same predicate but only warns for pool orders:
Root-only v1 wisps (phase = "vapor" without pour) pass the predicate: the root bead is itself Ready-visible work and may be routed to pools.

Deprecated surfaces

Last modified on October 29, 2026