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.
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 informulas/ 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:
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) resolvedgc formula show/cook/slingfirst-wins, letting imported pack formulas shadow same-name city overrides. If an override does not take effect, inspectgc 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.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.
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.
{{key}} placeholders substitute into descriptions, titles, notes,
assignee, and metadata values. Values are supplied as key=value pairs at
instantiation:
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 stepcondition 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:
max budget as a loop: label on the first body step:
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.
phaseis taken from the child, else the first parent that declares one.pouris monotonic: any ancestor’spour = truesticks; a child cannot opt out.contractandrequirescome 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
extendschains must fail (circular extends detected: a -> b -> a).
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.
1.8. Description Files
Usedescription_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:
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_filebest-effort: the reference is left unconsumed and any inlinedescriptionis 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.formulaname is required.contract, if set, must be"graph.v2"(contract: invalid value "<value>" (must be graph.v2)).typemust beworkflow,expansion, oraspect.- A var must not combine
required = truewith adefault. - Step
idis required and globally unique, includingchildren. - Step
titleis required unlessexpandis set. prioritymust be 0–4.depends_on/needsentries must reference known step IDs (including children).waits_formust matchall-children,any-children, orchildren-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))).timeoutmust be a positive Go duration and requirescheck— both v2-only; a baretimeoutin a v1 formula fails with a message routing convergence gate scripts toconvergence.gate_timeout/--gate-timeout.- Loops (validated during control-flow expansion):
bodynon-empty; exactly one ofcount/until/range;maxrequired withuntil.
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 → resolveextends →
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 asgc.step_refmetadata. - 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 withchildrenis promoted to issue typeepic. - Blocking edges.
needs/depends_oncompile toblocksedges;waits_forcompiles to a readiness-blockingwaits-foredge plus agate:<value>label on the cooked step (section 4). - Gate synthesis. A
[steps.gate]table synthesizes a gate bead (recipe ID<formula>.gate-<step>, typegate, titleGate: <type> <id>) that is a child of the step’s container, plus ablocksedge 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 atitlevar is declared; its description is the formula description, or{{desc}}when adescvar is declared. - Root-only wisps. When the formula sets
phase = "vapor"withoutpour, or has no steps, the recipe is root-only: the root is typetaskstampedgc.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 formuladescription. Settingpour = trueforces full materialization (checkpoint recovery) regardless of phase. - Step-type coercion at instantiation. Non-root step beads typed
task(the default) are stamped typestepsoReady()andbd readyskip 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) andgc.formula_sourceon the root;gc formula version-check <bead-id>compares the stored hash against the on-disk recipe to detect formula drift since spawn.
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):
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:
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
moleculecontainer root nor its default-typedstepbeads surface throughReady()(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 theissuevariable and routing variables specified in section 1.4. Formula slings do not create an auto-convoy; convoys, where they exist, track members through non-blockingtracksedges 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
untilloop validates the condition and writes aloop:{"until":...,"max":...}label on the first body step (the loop expander ininternal/formula/controlflow.go), but no component — neither the v1 cook path nor the v2 control dispatcher — reads that label. Anuntilloop therefore runs exactly one iteration. - Gate
typevocabulary.[steps.gate]synthesizes a real gate bead that blocks its step until the gate bead is closed (manually or by an external watcher), but thetypevaluesgh:run,gh:pr,timer,human, andmailare doc-comment vocabulary ininternal/formula/types.go— the parser never validates them and no bundled watcher acts on them. Zero bundled formulas usegate. waits_forgate modes.waits_forcompiles to a readiness-blockingwaits-fordependency edge plus a recorded gate mode, but no current component interprets theall-children/any-childrendistinction. Zero bundled formulas usewaits_for.vars.<name>.type. The variabletypefield (string,int,bool) is parsed into the variable definition (internal/formula/types.go) but never enforced; onlyrequired,enum, andpatternare validated at instantiation.
5. Conformance And Compatibility
Contract selection
v1 is the default contract. The host’s formula compiler capability is2.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 usescheck, 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:
<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:
phase = "vapor" without pour) pass the predicate:
the root bead is itself Ready-visible work and may be routed to pools.