skip to content

Among the orchestration formats

Where this format sits against Ansible, CI pipelines, Airflow, Temporal and the rest, and the design choices argued as positions rather than listed as features.

Where Gantry Patterns stands relative to the orchestration systems a technical reader will compare it against, with the design choices argued as positions rather than described as features. Written 2026-08-16; claims about external systems rest on a web survey performed in July 2026 and not re-searched since, so the statements about Conductor, gh-aw, and the Ralph-loop tools carry that date.

1. The problem: workers that can report success they did not achieve

Every orchestration format in production use — Ansible playbooks, CI pipeline configs, Airflow DAGs, Temporal workflows, Amazon States Language — orchestrates deterministic steps, and a deterministic step that fails says so: an apt module returns an error code, a compile step exits non-zero, an activity throws. Failure handling can therefore be an afterthought in a vocabulary that provides its constructs elsewhere — for inventory, for triggers, for scheduling, for retry backoff.

A coding agent is stochastic and able to report success it did not achieve: it can end its session declaring a task complete while the tests are red, write a summary describing work it did not do, or produce a file whose contents are wrong and plausible. An orchestrator that trusts the worker's own account compounds the error, because the next step builds on the unverified claim.

When the workers are unreliable, verification and recovery are the core vocabulary. For every step the orchestrator must answer four questions: what runs next, how the step is known to have worked, what happens when it did not, and when the run is done. Deterministic-worker formats leave the second and third implicit; a format for agent orchestration is organized around them.

Gantry Patterns is that format. A pattern is a TOML file answering all four. Gantry executes it with a deterministic interpreter containing no LLM: every intelligent act is a fresh, clean-context agent subprocess, and every truth claim is checked against deterministic evidence — a test command's exit code, a declared file's presence, the state of the git tree — before the run proceeds. One assumption: the workspace is a git repository and truth is an exit code. That covers more than code — a data-curation pipeline's gate is its validation scripts and its roster is task files derived from database entries — but a workflow with no repository and no checkable truth is out of scope, on purpose.

2. The format in one page

The whole format is nine primary words: pattern (a named sequence of steps and parameters); the producing step kinds agent, gate, and command; the composition words for, if, stop, and call; and the value word output. There is no expression language, no arithmetic, no variables, no counters, and no author-defined control operators; unknown fields are load errors everywhere.

The division of labor is an axiom: agents supply intelligence, deterministic checks supply truth, files hold state. The pattern only sequences. An agent step runs one fresh untrusted agent session. A gate step runs Gantry's authoritative project check package — baseline checks, red/green verdict handling, merge re-gates. A command step runs a deterministic command and records its declared outputs. Every step that produces evidence declares it as an output — a file or directory path produced by exactly one step, optionally carrying a closed values vocabulary that makes it branchable. if routes on exactly one fact: a declared output's value, checked at load time against that output's declared vocabulary; a step's done/failed outcome; or absence.

Three further commitments:

  • Roster-as-directory. Every list is a directory of task files. for runs a body pattern once per file, sequentially, in sorted order, and a directory an agent step declares as an output is a list the for can name. There is no second list source and no language-level list filter; completed tasks are skipped by the run record's interpretation, not by pattern logic.
  • Self-call as the only loop. There is no repeat, until, loop, cycle count, retry budget, or depth guard. Unbounded repetition is a recursive call, and the loader validates that every call participating in a cycle of the call graph is in tail position — a correctness rule, since a non-tail recursive retry could unwind into stale post-retry work. A recursive run ends when ordinary data routes to a pattern that falls off its end or to stop.
  • Run identity and freeze semantics. At run start, the engine copies the resolved pattern closure, reachable prompts, and inline script sources into the run directory, and folds the frozen closure and bound inputs into a content hash that is the run's identity. An edited pattern never silently changes a resumed run. The whole closure is validated at start — an unparseable included file, a non-exhaustive branch table, a duplicate name, or a non-tail recursive call fails at t=0, not hours in.

3. The survey

Nothing surveyed is the same kind of thing: a closed, computation-free, file-state format for gate-grounded agent orchestration with frozen run identity.

Ansible playbooks

Ansible is the original inspiration: process definitions as files to read, diff, fork, and hand to someone. Gantry Patterns keeps that property and almost nothing else. Ansible's vocabulary provides constructs for inventory, ordering, and idempotence, because its workers — modules applying state to hosts — are deterministic and convergent: running a task twice is safe, and a failed task says it failed. Neither property holds for an agent, so a pattern provides its constructs for declared outputs, gates, and routing instead. Ansible also grew what its problem demanded and Gantry's refuses: Jinja2 templating, when: expressions, registered variables, loops with computed lists. Those are the right tools for parameterizing configuration across a fleet, and they are the computation the no-computation axiom excludes.

CI pipeline configs (the GitHub Actions family)

CI configs orchestrate commands that fail honestly, so their vocabulary is about triggers, caching, matrices, and artifact storage; verification is the pipeline's entire body (the tests are the job), not a per-step doctrine. A CI run is stateless and event-shaped — one trigger, one pass through the graph, no resume, no loop-until-green; a Gantry run is a multi-hour stateful process with freeze, resume, and recursion. And the GitHub Actions expression syntax (${{ }}) is the canonical example of the small conditional feature this format refuses: a textual condition that widened, release by release, into a general expression system, because nothing in the design forbade it.

Airflow DAGs

Airflow's vocabulary provides constructs for scheduling: DAGs of operators, cron-shaped triggers, backfills, sensor waits, executor pools. It assumes tasks whose success signal is trustworthy and whose interesting failure mode is infrastructural — a worker died, a dependency was late. An Airflow DAG is a Python program: the definition is code that must be evaluated to be inspected, which is the data-versus-program choice the Starlark entry below treats directly. A pattern is data — diffable, pinnable, hashable into a run identity, and exhaustively validated at load, none of which a definition that is a program admits.

Temporal workflows

Temporal is the precedent for one specific commitment: durable execution with deterministic replay, where a workflow's history is a first-class recorded artifact and the code that resumes it must be deterministic. Gantry's run identity and freeze semantics apply that precedent to agent orchestration. A Temporal workflow is general-purpose code in a host language, orchestrating activities that are trusted to report their own outcomes; determinism is a discipline imposed on the author. A pattern is closed data, orchestrating agents that are trusted with nothing; determinism is a property of the interpreter, not an obligation on the author. Temporal also has what Gantry lacks and section 5 concedes: replay testing, a cheap way to exercise a definition's paths without running the workers.

Amazon States Language / Step Functions

ASL is the closest format precedent: JSON, closed state types (Task, Choice, Map, Parallel), declarative Retry/Catch policies, originally no computation. Gantry Patterns is "ASL for coding agents, with git as state and a test suite as truth." ASL grew Choice-rule comparison operators and later JSONata, because deterministic-worker orchestration keeps finding legitimate needs for a computed predicate. Gantry Patterns is more austere than that precedent: if refuses any comparison but a declared closed vocabulary, its only loop is recursion, and the standing answer to every computed predicate is a command step. The bet is that agent orchestration, unlike Lambda orchestration, does not need the predicates. The academic Workflow Patterns literature (van der Aalst) uses "patterns" for control-flow vocabularies; Gantry's use of the word is narrower — a pattern is one authored orchestration file, not an entry in a control-flow taxonomy.

Starlark / Bazel — the data-versus-program choice

Bazel faced the same tradeoff this format faced — a build definition must be deterministic and analyzable, and users keep needing more expressiveness — and answered it differently: a hermetic language rather than no language. Starlark is deliberately restricted Python — deterministic, terminating, sandboxed — and it is the strongest version of the question "why not a small safe language?" A Starlark file must be evaluated to be inspected: what a BUILD file declares depends on what its .bzl macros compute, and the analyzable artifact is the evaluation's output, not the file. A pattern is the artifact — the file the author wrote is the thing the loader validates, the diff reviewers read, the closure the run hashes — which is what admits the load-time checks and the frozen identity of section 2. A hermetic language would trade those for expressiveness the format's two standing answers (section 4) already cover.

Erlang/OTP supervision trees and Make — precedents, not competitors

Erlang/OTP supplies the division of labor. OTP's arrangement is that the generic machinery — supervision, restart strategies, message handling — is written once and deterministically, applications fill in only the specific parts, failure is expected, and the restart strategy is the design surface. Gantry Patterns is that arrangement for agent work: the engine owns the machinery (task iteration, resume, freezing, the records), patterns declare which evidence to route, and worker failure is a routine failed outcome, not an exception.

Make supplies the state model. A pattern has no variables because steps communicate through named files, exactly as Make targets do. Files are the most reliable interface an agent has: writing them is a harness's ordinary mode of work, while an agent's final message degrades badly under truncation. Files make every run a post-mortem artifact, since the run directory contains every intermediate state. And a declared file contract makes routing checkable — a step that was supposed to produce a file, and didn't, is a routable absence rather than a silent gap.

The agent-orchestration field (as surveyed July 2026)

Microsoft Conductor (announced May 2026) is the closest in intent: an MIT-licensed CLI running declarative YAML multi-agent workflows, motivated by "deterministic routing instead of LLM-based routing" — the same thesis as Gantry's no-LLM interpreter. Conductor carries Jinja2 templating and expression evaluation (the computation Gantry excludes); it targets general agent workflows such as research synthesis and design review rather than repository-grounded work; it has no gate or ground-truth concept; and it documents no freeze, resume, or run identity. The failure routing of section 4 — three routable facts, engine-forced stop for facts the pattern does not route — has no counterpart there.

GitHub Agentic Workflows (gh-aw, GitHub Next) is the closest in ecosystem position: Markdown files with YAML front matter defining agent jobs, compiled by a trusted compiler into hardened, SHA-pinned Actions workflows, harness-pluggable across Copilot, Claude, Codex, and Gemini. It shares Gantry's harness-neutrality and its compile-time validation. But it is event-triggered CI automation: one agent per trigger, no loop-until-green, no repair escalation, no fan-out with a systemic stop, no resume.

The Ralph loop lineage is the direct precedent for the problem framing. Geoffrey Huntley's Ralph technique (mid-2025) — loop a fresh-context agent against a task until external verification passes — had become, by mid-2026, a class of tools: HumanLayer's RPI methodology, and tools such as LoopTroop (work items in isolated git worktrees, a Ralph loop retrying failures with fresh context), Zeroshot (planner, implementer, and independent validators looping until a change is verified), and Loki Mode (a verification gate plus a review council that can veto "done"). These share one doctrine — fresh context plus iteration plus external verification — but every one of them hard-codes its loop, and none has a user-authorable orchestration format. Gantry's answer is the authorable pattern rather than a built-in Ralph loop: the shipped until.toml is that loop written in the nine words, and a user who disagrees with any of its choices forks the file.

In-process agent frameworks — LangGraph, CrewAI, AutoGen / Microsoft Agent Framework, Mastra, and the 2025–26 academic declarative-agent-DSL papers — orchestrate LLM calls inside one application process: state is objects, verification is optional, orchestration is code. Gantry orchestrates whole harness subprocesses against a filesystem. This is a different layer, and the comparison mostly serves to keep the work from being filed under "yet another agent framework."

4. The design choices, argued

The failure doctrine

This section, not the file format, carries the design's central claim.

The format keeps three facts about a step structurally distinct, and routes each by its own construct. A produced value is routed by if = "output_name" with a branch table that the loader checks for exhaustiveness against the output's declared values vocabulary — every declared value appears exactly once, and no other key appears. An absent output is routed by that if's else arm — absence is never a value, never a malformed record, and never an implicit fall-through. A failed step is routed by if = { step_name = "failed" } — a dead session, a malformed required output, a red gate tested through outcome routing. Malformed authored data is failure, not a synthesized value; a missing optional output is absence, not failure. And anything a pattern does not route reaches an engine-forced stop and the header's on_stop hook.

An agent that declares an output with values = ["complete", "retry", "blocked"] and writes probably-done has not produced a fourth value to be defaulted — it has failed, and the failure routes as a failure. A reviewer step designed to usually write nothing (the shipped sprint's troubleshoot_revert output) declares that absence route explicitly, so the common case is a decision, not a gap. The residual question every pattern author must answer: does anything downstream check this step's work? A step whose failure does not matter says so visibly, in the idiom if = { <step> = "failed" } then = "nothing", routing the failure to the engine-provided identity pattern nothing; the retired format said the same thing as a policy enum (note-and-continue), and the redesign dropped it because a reviewer reading the file sees the tolerance, where a policy attribute could be set once and never read again.

Exception-style handling (Temporal, most code-first frameworks) collapses the three facts into one thrown object and lets an unhandled case propagate to a generic handler. Retry/Catch policies (ASL) are declarative but attribute-shaped: the failure handling is metadata on the state, not a visible route in the flow, and absence is not a concept at all. CI configs mostly have continue-on-error. None of these forces exhaustiveness, and none distinguishes "the worker said no" from "the worker said nothing" from "the worker's answer was not one of the answers".

The no-computation axiom and its standing answers

The rule: a pattern performs no computation. The objection is that every "no expressions" configuration format eventually acquires them — GitHub Actions, Amazon States Language, and Helm each began with a small conditional feature and widened it into an expression system. The answer is that the format removed the conditional feature rather than resisting the widening: the retired format carried one textual-equality condition (flag), and the redesign deleted it instead of porting it. if tests exactly one fact, and there is no numeric predicate, no parameter comparison, no list-emptiness check, and no compound condition anywhere for such a feature to grow from.

Every need for computation has one of two standing answers. A judgment — is this plan sound, is there work left, is this diff acceptable — is an agent step: the worker that knows says so, in a declared output with a closed vocabulary. A deterministic derivation — how many consecutive tasks failed, is the roster well-formed — is a command step that derives the number, reduces it to a one-word declared output, and discards the count; the shipped map's streak stop works this way, and numbers are never pattern state. The remainder is honest: the computation moved into run shell text the loader cannot analyze, so the analyzability boundary sits at the command step. A command step's decision surface is still a closed declared vocabulary the loader checks, so what the pattern branches on stays analyzable even where the derivation is not.

Fork-by-copy, and its honest costs

Variance in Gantry Patterns is a copied file with an edited line and a new name. map.toml's check_streak command carries the streak threshold that stops the run after two consecutive failed tasks; change that line in a copy and the copy stops after one instead. The test the design was held to: if two meaningfully different orchestrations cannot differ by one line, the result is a configuration format, not a language for authoring orchestrations.

The cost is real: forks drift. When a shipped pattern improves, every local fork keeps the old behaviour, and there is no patch or overlay mechanism to pull the improvement through. This is the Helm-fork versus Kustomize-overlay debate, and that ecosystem drifted decisively toward overlays. The include composition does not change the verdict: duplicate pattern names across a composed closure are load errors, so a file cannot include a shipped pattern and override one definition inside it — variance is still a copy, and the idiom for removing behaviour is a separate pattern declaring its own definitions rather than a flag that switches a step off. The mitigations are parameters (a fork that only changes a prompt or a path is not a fork at all), include composition (a variant file can reuse every pattern it does not disagree with), and the convention that a variant differs by one consequential line, which keeps the diff between a fork and its parent small enough to re-apply by hand when the parent moves. These are partial. Overlays buy upgrade propagation by making the effective definition a merge no one authored — the exact property fork-by-copy exists to avoid.

TOML as host, and its honest costs

A pattern is TOML because a pattern is data, and TOML is a diffable, universally parseable host for data with comment support. The costs are two, and both are visible in the shipped corpus.

The first is fragmentation. Every if arm names a pattern, so control flow that a language would write as nested blocks becomes many small named patterns: the shipped sprint.toml is twenty-seven patterns where the retired format's file was one, and some of them (route_troubleshoot_verdict, regate_after_review_repair) are continuation labels called patterns — structured goto rather than goto's absence. The trade is judged worth it, because every arm is named, commented, and visibly entered — but the reader reconstructs the graph across the file, and gantry pattern show, a static pattern diagram, exists because that reconstruction needs mechanical help.

The second is the tail-position obligation. Because a loop is never a literal loop word but a cycle through arm patterns (until_loopdecide_cycleclose_the_gaprun_cycleuntil_loop), a reader must trace the chain to see the loop, and an author writing any retry must understand the rule the loader enforces: every call participating in a cycle must be in tail position on every path, or the caller's remaining steps would run again when the recursion unwinds. The violation is caught at load, so the obligation costs an error message rather than a corrupted run — but it is a concept an author must hold that while would have spared them.

Against both costs stands what data allows — the load-time checks and the frozen identity of the Starlark section above. A YAML host would change nothing in this argument (and inherit YAML's parsing ambiguities); an HCL or Starlark host would trade the argument away.

What is deliberately absent

  • No human-approval step. Spec § 12: "The pattern language does not model operator action." A stopped run records the prefix Gantry witnessed; resume applies the same frozen pattern from the last recorded boundary, and the record does not carry what the operator did while the run was stopped. Operator stop and resume cover the pause; the refusal of an approval step is doctrinal, because an operator decision inside the flow would make the run's record depend on an unrecorded actor.
  • No concurrency in for. A for is sequential: it is not a parallel map and carries no concurrency property. The tasks share a git worktree and a run record. Sequentiality is also what makes roster order equal finish order, which the streak-stop idiom depends on.
  • No recursion ceiling in the grammar. There is no depth guard, cycle count, attempt_ceiling word, or max_cycles word — removed during the redesign, on the argument that depth was a proxy for spend and no one number fits both task retries in sprint and goal cycles when one run contains both (until calls build, which reaches sprint through milestone's for body). Recursion ends when a routed verdict ends it; an operator can still pick an authored ceiling when the selected pattern declares one. The shipped sprint pattern declares attempt_ceiling and the shipped until pattern declares max_cycles, both defaulting to unbounded. Section 5 records the counter-reading.
  • No variables, no expressions, no user-defined step kinds, no LLM in the interpreter. Retry on a transient 529 and the lifecycle controls are runtime concerns invisible to patterns. Agent steps may declare their stage and optional model, effort, or harness overrides; a prompt id never selects a role, stage, branch, or recovery policy.

The gate and the completion contract are engine-owned. The gate step carries Gantry's authoritative project check package, which no command can reproduce, and no pattern declares a commit, DONE-row, ledger, or task-completion operation; recording task completion is part of interpreting for. The retired format drew this line badly, with seven privileged engine operations (inner-unit) that patterns could invoke but never reimplement; the redesign retired all of them, and the bundled corpus is authored entirely in the nine words. A pattern that could write half of the completion contract could record a task complete that was not.

5. Open weaknesses

These are the criticisms the July 2026 review made that the redesign did not answer, re-verified as still standing on 2026-08-16.

  • Fork-by-copy drift is unmitigated at ecosystem scale. The costs section above states a defense, not a solution: there is no mechanism for a fork to receive an upstream improvement, and the Helm/Kustomize history suggests ecosystems under this pressure grow overlays whether the format permits them or not.
  • There is no dry-run. Load-time validation catches structural errors, but a pattern's behaviour is only observable by spending agent-hours; there is no way to walk a pattern with stubbed step results — agent fails, gate red, gate red again — and see the path taken. Temporal has replay testing; Conductor ships a dry-run mode. For a format whose claim is deciding failure handling up front, exercising the failure paths without a live run is the biggest gap. It is cheap to close: the interpreter is written against a World trait whose every effect method has a default implementation (src/engine/run/interpreter.rs), and the interpreter's own tests already drive patterns through a ScriptedWorld — a dry-run CLI verb would be a thin wrapper over machinery that exists.
  • Under for with fail = "continue", an unrouted body failure writes no verdict file. A task whose own gate fails is routed by the body and records its verdict; a task whose session dies is a fact the pattern does not route, recorded by the engine but absent from the pattern's own verdict files — so a systemic fault, such as a dead harness failing a 200-task roster one session at a time, does not feed a pattern-authored streak stop. The redesign accepted the gap, on the reasoning that a dead session is a different signal from tasks failing their own gates, with an engine-writes-a-verdict fallback held in reserve; it is a real gap until then.
  • The only loop has no bound in the grammar. The no-recursion-ceiling decision is argued above, and authored parameters can bound selected patterns — but a reviewer may fairly read "a troubleshoot pass that keeps declaring retry is bounded only by lifecycle controls unless the pattern authors its own stop" as the format making termination a policy choice rather than a grammar guarantee.
  • The analyzability boundary sits at the command step. Stated in section 4: the shell text in run is as unanalyzable as any shell, and the format's guarantee is only that what patterns branch on is closed and load-checked, not that what commands do is.

docs/patterns/positioning.md in the Gantry repository, rendered as it stands