Skip to content

WF-3 format & lifecycle decisions

Date: 2026-06-19 Status: Locked (gates T002–T006) Source spec: docs/specs/2026-06-19-anvil-workflow-substrate.md §11 Task: T001 — Lock WF-3 spec-format and lifecycle decisions

This note resolves the five §11 open questions before any runner code is written, so T002 (schema/parser), T003 (runner), T004 (proof gate), T005 (fan_out), and T006 (concurrency proof) build against a fixed target.


Decision: spec format — minimal YAML, scoped to non-PRD loops

WF-3 authors ad-hoc loops in a small .anvil/workflows/*.yaml surface. PRD-markdown remains the front door for greenfield work. Spec §11 cautions against a second authoring surface "unless non-PRD loops clearly need it" — WF-3 is exactly that case (the ~75% brownfield/ad-hoc work the PRD front door does not launch), so a deliberately tiny YAML is justified. The format stays minimal (see the control-flow ceiling below); it is not a general programming surface.

Decision: task lifecycle — CREATE ephemeral, run-scoped tasks

A workflow run CREATES and claims its tasks; fan_out derives N run-scoped tasks from a step's typed output (spec §9). The common WF-3 case ("one task per flaky test") is inherently create-on-the-fly and cannot be expressed by binding to pre-authored PRD task IDs. Bind-to-existing is deferred — added only if a concrete non-PRD loop earns it. For WF-1/WF-2 this is moot (they drive the PRD's existing ready queue).

Decision: trigger ownership — delegated to the harness

Triggering (cron, schedule, on:) is delegated to the harness, not owned by the spec or a WF-3 daemon. This is already the WF-2 posture (§6: Codex automation schedule, GitHub Action on:) and is consistent with the out-of-scope line (§12: "a long-running daemon, scheduler, or DAG engine"). anvil run-workflow runs steps and exits; whatever fires it owns the cadence. Spec-declared cron is reconsidered only if a runtime cannot supply its own schedule.

Decision: control-flow ceiling — minimal primitives + code escape-hatch

The declarative format supports exactly run, fan_out, needs, and on_fail: reopen. Anything richer (conditionals, general loops, map-reduce DSL) is out — a step that needs it drops to a code workflow via the escape-hatch. This holds the line spec §11 and §3.3 / SL1-RR-1 §6 draw: "resist growing a DAG engine." The ceiling is the test for "just code": if a workflow wants if/when or while, it has crossed the line and should be code, not YAML.

Decision: concurrency acceptance bar — mirror the existing suite

T006 proves fan_out stresses the wedge with the same rigor as tests/test_claims_concurrency.py: N parallel fan_out claims over overlapping expected_files must yield exactly one winner per task and zero lost evidence, across ≥200 iterations, ≥8 threads, and multiple contention shapes. A single-pass smoke test is insufficient — the rare races this wedge guards against only surface under iteration.


Locked primitive set (for T002's schema)

Primitive Purpose
run a single governed step (claim → run → capture proof → submit → apply)
fan_out derive + create + claim N run-scoped tasks from a step's typed output
needs ordering dependency between steps
proof typed per-step gate — a passing CommandProof must exist (SL-3 ProofRequirement)
on_fail: reopen per-step loop-until policy
uses_code escape-hatch: a step delegates to a code workflow when declarative is insufficient

Explicitly excluded: if/when conditionals, general while/loop constructs, and any map-reduce/DAG DSL. These are the "just code" boundary.