Skip to main content
FieldValue
StatusAuthoritative specification
Last verified2026-06-12
Contractformula_compiler 1.0 (default — no declaration required)
Primary implementationinternal/formula, internal/molecule
User-facing guideUnderstanding Formulas
TutorialFormulas tutorial
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

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. 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.
formula (TOML)
  → compiled recipe (flat list + dependency edges)
    → molecule container root      (type "molecule")
    + child step beads             (parent-child edges to their container;
                                    blocking edges from needs/depends_on)
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 controller 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 controller’s control dispatcher executes only control beads, which v1 compilation never emits. The execution model is the structural difference from v2:
v1Formulas v2
Compiled shapeParent-child molecule tree under a molecule container rootFlat graph: task root plus step beads linked only by blocking dependency edges
Runtime engineNone. Conditions and loops resolve at cook time; afterwards the molecule is inert dataThe controller’s control dispatcher executes every control bead — check and retry evaluation, fan-out, tally, drain, scope checks, workflow-finalize
Who advances workAgents working hooked beads, inside their own sessionsThe controller drives orchestration outside any agent session; agents only run plain work beads
Agent fan-outThe molecule is typically worked by the one agent it is slung to; spreading steps across agents is manual routingStep beads are independently routable; per-step routing intent resolves at dispatch, and drain / on_complete fan out across agents or pools at runtime
Root visibilityThe container root is the molecule’s handle; default-typed steps are stamped type step and excluded from Ready() (section 2)Step beads are independently Ready-visible and routable; the root surfaces only when the workflow completes
Dependency semanticsA dependency on a parent gates on the parent and its children (container dependencies, section 1.3)No parent-child edges; a dependency on a parent gates only on the parent step bead
A minimal v1 formula — note the absence of any contract declaration:
formula = "pancakes"
description = "Make pancakes from scratch"

[[steps]]
id = "dry"
title = "Mix dry ingredients"
description = "Combine flour, sugar, baking powder, salt in a large bowl."

[[steps]]
id = "wet"
title = "Mix wet ingredients"
description = "Whisk eggs, milk, and melted butter together."

[[steps]]
id = "combine"
title = "Combine wet and dry"
description = "Fold wet ingredients into dry. Do not overmix."
needs = ["dry", "wet"]

[[steps]]
id = "cook"
title = "Cook the pancakes"
description = "Heat griddle to 375F. Pour 1/4 cup batter per pancake."
needs = ["combine"]

[[steps]]
id = "serve"
title = "Serve"
description = "Stack pancakes on a plate with butter and syrup."
needs = ["cook"]

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, tally, 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:
FilenameStatus
formulas/<name>.tomlCanonical
formulas/<name>.formula.tomlAccepted deprecated spelling; the .formula infix is not part of the formula name
formulas/<name>.formula.jsonLoader-only deprecated fallback; excluded from symlink staging
Formula directories are collected into layers, ordered lowest to highest priority:
LayerDirectory
1formulas/ directories from city packs (imported packs)
2The city’s own formulas/ directory
3formulas/ directories from rig packs
4The rig’s formulas_dir (rig-local override declared on [[rigs]]; relative paths resolve against the city directory)
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:
[formulas].dir is no longer supported; use the well-known formulas/ directory
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

KeyTypePurpose
formulastringRequired. Unique formula name used by gc formula cook, gc sling --formula, and molecule.Cook/CookOn
descriptionstringHuman-readable description; supports {{var}} substitution
requirestableHost capability requirements. formula_compiler (a semver comparator) is the only axis; unknown axes fail with formula.requirement_unknown (section 5). Typically absent from a v1 formula; a constraint that capability 1.0.0 satisfies keeps the formula on v1
contractstringDeprecated v2 opt-in. Only valid value: "graph.v2"; anything else fails validation. Declaring it moves the formula to the v2 contract (section 5)
extends[]stringParent formulas to compose from (section 1.7)
varstableTemplate variable declarations (section 1.4)
steps[]tableWork items to create (section 1.3)
typestringworkflow (default), expansion, or aspect
phasestringv1-era materialization hint: "liquid" (pour) or "vapor" (wisp). phase = "vapor" without pour compiles a root-only recipe — steps are not materialized as beads. Kept for compatibility; not a design surface for new formulas
pourboolMaterialize each step as a bead row (checkpoint recovery). Default false. Monotonic through extends: any ancestor’s pour = true sticks
catalogtable{name, description} opting the formula into workflow-catalog discovery (gc formula catalog)
template[]tableExpansion template steps for type = "expansion" formulas ({target} / {target.description} placeholders)
composetableAdvanced composition rules: bond_points, hooks, expand, map, branch, gate, aspects (section 1.7)
advice[]tableAdvanced before/after/around step transformations applied during cooking
pointcuts[]tableAdvanced target patterns for type = "aspect" formulas
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.
KeyTypeDeclarationPurpose
idstringRequired. Unique across the whole formula, including children
titlestringRequired unless expand is set; supports {{var}} substitution
descriptionstringStep instructions shown to the agent; supports {{var}}
description_filestringPath to a file whose contents replace description (section 1.8)
notesstringAdditional notes; supports {{var}}
typestringIssue type: task, bug, feature, epic, chore (conventional vocabulary; not validated)
priorityint0–4; out-of-range values are rejected
tags[]stringLabels applied to the created bead. The TOML key is tags; deprecated JSON formulas spell it labels — a TOML labels key is silently ignored
metadatatableString key/value pairs copied to the cooked bead. gc.* keys are reserved for the runtime; several force the v2 declaration (section 5)
depends_on[]stringStep IDs this step blocks on; must reference known IDs
needs[]stringSimpler alias for depends_on; both are real and merged during cooking
conditionstringCompile-time include/exclude (section 1.5)
children[]stepNested sub-steps; IDs share the formula-wide namespace. A step with children compiles to an epic container (section 2)
assigneestringDefault assignee; supports {{var}}
expandstringInline an expansion formula here (the step is replaced by its template steps)
expand_varstableVariable overrides for the inline expansion
looptableIteration container (section 1.6)
waits_forstringFanout gate: all-children, any-children, or children-of(step-id); the referenced step must exist (sections 2 and 4)
gatetableAsync wait condition {type, id, timeout} (sections 2 and 4)
checktablev2-onlyInline run/check verification loop
retrytablev2-onlyTransient retry loop
draintablev2-onlyScatter the input convoy into unit convoys
on_completetablev2-onlyRuntime fan-out over step output
tallytablev2-onlyAggregate fan-out voter outputs; requires on_complete
timeoutduration stringv2-onlyMax duration for a check script; requires check
Compiling a v1 formula that uses any v2-only construct must fail with:
requires: formulas that use graph-only constructs must declare [requires] formula_compiler = ">=2.0.0" or the deprecated contract = "graph.v2" explicitly
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.
formula = "deploy"
description = "Deploy {{env}} from {{branch}}"

[vars]
branch = "main"

[vars.env]
description = "Deployment environment"
required = true
enum = ["dev", "staging", "prod"]

[[steps]]
id = "deploy"
title = "Deploy {{env}}"
Table-form fields:
FieldTypePurpose
descriptionstringWhat the variable is for; shown by gc formula show
defaultstringValue used when none is provided. An explicit empty string is a valid default
requiredboolThe variable must be provided at instantiation. Declaring required = true together with a default fails validation: vars.<name>: cannot have both required:true and default
enum[]stringAllowed values; enforced at instantiation
patternstringRegex the value must match; enforced at instantiation
typestringParsed but not enforced (section 4)
{{key}} placeholders substitute into descriptions, titles, notes, assignee, and metadata values. Values are supplied as key=value pairs at instantiation:
gc sling worker deploy --formula --var env=prod
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:
FormMeaning
{{var}}Include when the value is truthy
!{{var}}Include when the value is falsy
{{var}} == valueInclude on equality (quotes around value are stripped)
{{var}} != valueInclude on inequality
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.
ModeKeysExpansion
countcount = NCompile time: the body is expanded N times, with iteration N+1 chained after iteration N
rangerange = "start..end", optional varCompile time: bounds support integers, + - * / ^, parentheses, and {var} substitution; var exposes the iteration value as {var} in body steps
untiluntil = "<condition>", max = N (required)Compile time: one iteration is expanded and its first body step is labeled with the condition and max budget for runtime re-execution — which no current runtime performs (section 4)
A range loop (the same shape works with count):
formula = "hanoi"

[[steps]]
id = "moves"
title = "Tower moves"

[steps.loop]
range = "1..3"
var = "move_num"

[[steps.loop.body]]
id = "move"
title = "Move {move_num}"
gc formula show hanoi renders the chained iterations:
Formula: hanoi

Steps (3):
  ├── hanoi.moves.iter1.move: Move 1
  ├── hanoi.moves.iter2.move: Move 2 [needs: hanoi.moves.iter1.move]
  └── hanoi.moves.iter3.move: Move 3 [needs: hanoi.moves.iter2.move]
An until loop expands a single iteration and records the condition and max budget as a loop: label on the first body step:
formula = "poll-until"

[[steps]]
id = "poll"
title = "Poll for completion"

[steps.loop]
until = "probe.status == 'complete'"
max = 5

[[steps.loop.body]]
id = "probe"
title = "Probe the endpoint"
Formula: poll-until

Steps (1):
  └── poll-until.poll.iter1.probe: Probe the endpoint
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; controller-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:
description_file = "../assets/workflows/review/local-review.md"
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 / tally field constraints); those 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):
Formula: pancakes
Description: Make pancakes from scratch

Steps (5):
  ├── pancakes.dry: Mix dry ingredients
  ├── pancakes.wet: Mix wet ingredients
  ├── pancakes.combine: Combine wet and dry [needs: pancakes.dry, pancakes.wet]
  ├── pancakes.cook: Cook the pancakes [needs: pancakes.combine]
  └── pancakes.serve: Serve [needs: pancakes.cook]
A step with children renders with an (epic) marker:
  ├── feature.build: Build the feature (epic)
  ├── feature.build.api: Implement the API
gc formula cook pancakes materializes the molecule; the created count is steps + root, and each bead gets an independent store ID:
Root: mc-8qi
Created: 6
pancakes -> mc-8qi
pancakes.combine -> mc-2x7
pancakes.cook -> mc-mjm
pancakes.dry -> mc-pzz
pancakes.serve -> mc-gzg
pancakes.wet -> mc-k1b
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. gc sling <target> <formula> --formula instantiates the formula as an ephemeral wisp and routes the root bead to the target:
Slung formula "pancakes" (wisp root mc-98o) → worker
A root-only vapor formula previews and cooks to a single bead:
Formula: patrol
Description: Patrol loop worked from the root bead
Phase: vapor
Root only: true

Steps (1):
  └── patrol.scan: Scan for stale work
Root: mc-lbd
Created: 1
patrol -> mc-lbd

3. Runtime

v1 has no runtime engine. The controller’s control dispatcher executes control beads only, and v1 compilation emits none — after instantiation no controller 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. The core pack’s reaper 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.
Declaration stateContract
No contract key, no formula_compiler requirementv1 (default)
[requires] formula_compiler constraint that capability 1.0.0 satisfies (for example ">=1.0.0")v1
[requires] formula_compiler = ">=2.0.0", or the deprecated contract = "graph.v2"v2 — out of this contract; see the v2 specification
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 tally, which requires it), 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:
requires: formulas that use graph-only constructs must declare [requires] formula_compiler = ">=2.0.0" or the deprecated contract = "graph.v2" explicitly
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:
convergence wisps do not support v2 formula "<name>"; use a v1 formula until convergence has an explicit input convoy target

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:
formula "<name>" root is a molecule container, not Ready-visible work; scale-from-zero pools will not wake for this wisp. Convert the formula to phase="vapor"/root-only or formulas v2 before routing it to a pool
Order dispatch applies the same predicate but only warns for pool orders:
warning: pool order "<order>" uses formula "<name>" whose root is a molecule container, not Ready-visible work; scale-from-zero pools will not wake for this wisp. Convert the formula to phase="vapor"/root-only or formulas v2 before routing it to a pool.
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

SurfaceStatusReplacement
contract = "graph.v2"Deprecated opt-in that moves a formula to the v2 contract; gc doctor warns: deprecated contract = "graph.v2"; use [requires] formula_compiler = ">=2.0.0"[requires] formula_compiler = ">=2.0.0" (for formulas that should be v2)
<name>.formula.toml / <name>.formula.jsonDeprecated spellings (section 1.1)formulas/<name>.toml
JSON labels step keyDeprecated JSON spelling (section 1.3)TOML tags
Last modified on June 13, 2026