Writing a pattern from scratch
Four stages, each one complete runnable file: a gated task, a decomposition, a map over a task directory, and an until pattern that recurses until a check passes.
Build four TOML patterns: a gated task, a decomposition, a map, and a recursive goal run. Read the pattern format for the normative reference, the authoring guide for authoring guidance, and orchestration patterns for the shipped corpus.
What you are building, and how to check it
A pattern is finite TOML data describing an orchestration: which fresh agent sessions run, in what order, and what happens when one fails or claims work it did not do. It carries no expression language. Every intelligent step is one fresh agent session with a clean context; every decision routes on a file an earlier step wrote or on a step's own outcome; the gate is Gantry's authoritative project check package, which no agent can override.
gantry pattern show <name|file.toml> --asciiloads a named pattern or TOML file through the loader and renders a static diagram; a file that does not load is refused with the loader's error, so this is also the validation check. Asked for outside any run, it resolves a name through the global config mirror~/.config/gantry/patterns/, then the bundled tier; a run consults a project-local.gantry/<plan>/patterns/<name>.tomlfirst. Save the file you are drafting into that mirror and show it by name.gantry triage <plan>prints which pattern that plan would run under and the whole registry, each entry with its tier and description.
To run a stage: gantry --pattern <name|file.toml> [inputs…] — the selector takes either a discovery name or a TOML file path, and inputs bind positionally to the entry pattern's parameters.
Stage 1 — execute, gate, review, on a single task
Save this as one-task.toml:
# ## one_task — one task: execute, gate, repair, review
[header]
pattern_language_compatibility = "3"
entry = "one_task"
description = """
One task: a fresh agent executes the plan, the gate judges the result, a repair agent gets one
attempt at a red gate, and a reviewer judges the finished change.
"""
[patterns.one_task]
[[patterns.one_task.parameters]]
name = "plan_source"
kind = "path"
default = "plan.md"
declared_plan_source = true
description = "The task prose the executor reads, and the run's identity."
[[patterns.one_task.steps]]
name = "execute"
type = "agent"
prompt = "execute"
inputs = ["{{plan_source}}"]
[[patterns.one_task.steps]]
name = "build_gate"
type = "gate"
command = "bin/gate"
[[patterns.one_task.steps]]
if = { build_gate = "red" }
then = "repair"
else = "review_task"
[[patterns.repair.steps]]
name = "repair_agent"
type = "agent"
prompt = "troubleshoot"
[patterns.repair.steps.text]
POSITION_CLAUSE = """
A previous agent left this tree red against the project gate. Repair its work rather than starting
over: make the smallest verified repair you can, and verify it before you finish.
"""
[[patterns.repair.steps]]
name = "repaired_gate"
type = "gate"
command = "bin/gate"
[[patterns.repair.steps]]
if = { repaired_gate = "red" }
then = "stop_still_red"
else = "review_task"
[[patterns.review_task.steps]]
name = "review"
type = "agent"
prompt = "review"
diff = "run-base"
outputs = [
{ name = "review_verdict", path = "state/review-verdict.txt", values = ["complete", "blocked"] },
{ name = "review_note", path = "state/review-note.md" },
]
[patterns.review_task.steps.text]
REVIEW_SUBJECT = "task"
[[patterns.review_task.steps]]
if = "review_verdict"
then = { complete = "nothing", blocked = "stop_review_blocked" }
else = "stop_missing_verdict"
[[patterns.stop_still_red.steps]]
type = "stop"
note = "The gate is still red after one repair attempt."
[[patterns.stop_review_blocked.steps]]
type = "stop"
note = "The reviewer declared itself blocked."
[[patterns.stop_missing_verdict.steps]]
type = "stop"
note = "The reviewer finished without writing review_verdict; a reviewer that wrote no verdict reviewed nothing, so the run stops rather than treating silence as approval."
Every [patterns.<name>] table is one callable pattern; this file defines six, and five exist only as branch targets.
The entry pattern one_task declares one parameter, plan_source, of kind path. declared_plan_source = true marks it as the input that supplies the run's identity — the file Gantry hashes to recognize this run again — and default = "plan.md" is what a caller gets without passing anything.
execute is an agent step: one fresh agent session, given the prompt id execute and shown {{plan_source}} as an input. build_gate then runs the project check with baseline checks and red/green verdict handling.
The if step that follows is the step-outcome form: if = { build_gate = "red" } tests the named step's outcome, using the gate color alias red. Its then and else arms each name a pattern: a red gate goes to repair, a green one to review_task.
The repair pattern is the failure path. A fresh agent runs with the troubleshoot prompt; POSITION_CLAUSE supplies its position clause. The step declares no outputs, so only troubleshoot's "repaired and verified" end state is reachable. Then the gate runs again, and the second if routes a still-red tree to a stop. This pattern gives repair one attempt; the shipped sprint.toml recurses instead.
review_task shows declared outputs. The reviewer declares two: review_note, a file contract, and review_verdict, a branchable value — a file that must contain exactly one member of its closed values vocabulary, here complete or blocked. diff = "run-base" is a boundary reference: the review span shown to the agent covers everything since the run's starting commit.
The final if is the output form, exhaustive by rule: the then table names every declared value exactly once. complete routes to nothing, the engine-provided identity pattern, so execution falls off the end and the run completes. blocked routes to a stop. The else arm handles absence: the session ended and the verdict file was never written. Absence is not a value and is never defaulted.
An unrouted failed outcome reaches the engine-forced stop. execute has no if = { execute = "failed" } arm, so an executor session that dies stops the run before build_gate runs. A failed step is recoverable only when an if says so.
Check and run it:
cp one-task.toml ~/.config/gantry/patterns/one-task.toml gantry pattern show one-task --ascii gantry --pattern one-task plan.md
Spec: § 2 File Shape, § 3 Header, § 6 Producing Steps, § 7 Outputs, § 8 if, § 12 stop.
Stage 2 — minimal decomposition
A planner agent writes milestone briefs into a directory, and a for step runs the Stage 1 pipeline once per brief. Save as split.toml:
# ## split — decompose a plan into milestone briefs, then run each through the stage-1 pipeline
[header]
pattern_language_compatibility = "3"
entry = "split"
description = """
Decompose a plan into ordered milestone briefs, then run each brief through the execute–gate–repair–review
pipeline. Split once, then iterate: no per-milestone planning pass, no replanning.
"""
[patterns.split]
[[patterns.split.parameters]]
name = "plan_source"
kind = "path"
default = "plan.md"
declared_plan_source = true
description = "The plan prose the planner reads, and the run's identity."
[[patterns.split.steps]]
name = "plan_milestones"
type = "agent"
prompt = "write-milestones"
inputs = ["{{plan_source}}"]
outputs = [
{ name = "milestones", path = "milestones/" },
{ name = "plan_scope", path = "state/plan-scope.txt", values = ["work", "none"] },
]
[[patterns.split.steps]]
if = "plan_scope"
then = { work = "build_briefs", none = "stop_no_work" }
else = "stop_missing_scope"
[[patterns.build_briefs.steps]]
name = "build_each_brief"
type = "for"
list = { directory = "{{milestones}}" }
body = "one_task"
[patterns.build_briefs.steps.parameters]
plan_source = "{{task}}"
# The stage-1 pipeline, now the body the `for` calls once per brief. The parameter lost its default
# and `declared_plan_source`: the `for` binds it, and run identity belongs to the entry pattern.
[patterns.one_task]
[[patterns.one_task.parameters]]
name = "plan_source"
kind = "path"
description = "The brief this invocation builds."
[[patterns.one_task.steps]]
name = "execute"
type = "agent"
prompt = "execute"
inputs = ["{{plan_source}}"]
[[patterns.one_task.steps]]
name = "build_gate"
type = "gate"
command = "bin/gate"
[[patterns.one_task.steps]]
if = { build_gate = "red" }
then = "repair"
else = "review_task"
[[patterns.repair.steps]]
name = "repair_agent"
type = "agent"
prompt = "troubleshoot"
[patterns.repair.steps.text]
POSITION_CLAUSE = """
A previous agent left this tree red against the project gate. Repair its work rather than starting
over: make the smallest verified repair you can, and verify it before you finish.
"""
[[patterns.repair.steps]]
name = "repaired_gate"
type = "gate"
command = "bin/gate"
[[patterns.repair.steps]]
if = { repaired_gate = "red" }
then = "stop_still_red"
else = "review_task"
[[patterns.review_task.steps]]
name = "review"
type = "agent"
prompt = "review"
diff = "iteration-start"
outputs = [
{ name = "review_verdict", path = "state/review-verdict.txt", values = ["complete", "blocked"] },
{ name = "review_note", path = "state/review-note.md" },
]
[patterns.review_task.steps.text]
REVIEW_SUBJECT = "task"
[[patterns.review_task.steps]]
if = "review_verdict"
then = { complete = "nothing", blocked = "stop_review_blocked" }
else = "stop_missing_verdict"
[[patterns.stop_no_work.steps]]
type = "stop"
note = "The planner reported no actionable work in the plan."
[[patterns.stop_missing_scope.steps]]
type = "stop"
note = "The planner finished without writing plan_scope, so there is no honest answer to whether the plan holds work; the run stops rather than iterating a directory nobody vouched for."
[[patterns.stop_still_red.steps]]
type = "stop"
note = "The gate is still red after one repair attempt."
[[patterns.stop_review_blocked.steps]]
type = "stop"
note = "The reviewer declared itself blocked."
[[patterns.stop_missing_verdict.steps]]
type = "stop"
note = "The reviewer finished without writing review_verdict; a reviewer that wrote no verdict reviewed nothing, so the run stops rather than treating silence as approval."
plan_milestones is a planner agent running the shipped write-milestones prompt, whose output contract names the milestones directory output this step declares. A path ending in / declares a directory output: a list, never a decision value, and the format's only list form. The planner also declares plan_scope, a branchable value that states whether the plan held work. An empty plan routes to stop_no_work rather than being inferred from an empty directory, and a planner that wrote no scope file routes through else to its own stop.
build_each_brief is a for step over list = { directory = "{{milestones}}" } — the directory the planner just wrote. Each iteration invokes the body pattern, here one_task, with the bindings in the step's parameters table: plan_source = "{{task}}", where {{task}} is the active task file path. The for is sequential and reads the directory when it starts. When a body invocation falls off its end, that task is recorded complete.
In one_task, plan_source has no declared_plan_source or default because split owns run identity and the for binds it. The reviewer's diff is iteration-start, so it covers one brief.
for has a fail property with the closed vocabulary stop (the default) and continue; it governs only what the body did not route. A brief whose repair still ends red routes to stop_still_red, and an authored stop ends the whole run whatever fail says. An unrouted failed body invocation also ends the run. Stage 3 uses continue.
What you just learned: the for step (list = { directory = ... }, body, parameters, fail); directory outputs as the format's only list form; the {{task}} reference; the iteration-start boundary reference. Spec: § 9 for, § 7 Outputs, § 13 Boundary References.
Stage 3 — a simple map
Point a for body at an existing directory of task files to make a map. Save as run-directory.toml:
# ## run_directory — run a pre-existing directory of task files, each carrying its own prompt and gate
[header]
pattern_language_compatibility = "3"
entry = "run_directory"
description = """
Run every task file in a pre-existing directory. Each file carries its own worker prompt and gate
command; a failed task is recorded and the run continues to the next.
"""
[patterns.run_directory]
[[patterns.run_directory.parameters]]
name = "tasks"
kind = "path"
declared_plan_source = true
description = "The directory of task files, and the run's identity."
[[patterns.run_directory.steps]]
name = "run_tasks"
type = "for"
list = { directory = "{{tasks}}" }
body = "carried_task"
fail = "continue"
[[patterns.carried_task.steps]]
name = "build_task"
type = "agent"
prompt = "{{task.prompt}}"
[[patterns.carried_task.steps]]
name = "task_gate"
type = "gate"
command = "{{task.gate}}"
[[patterns.carried_task.steps]]
if = { task_gate = "red" }
then = "repair_task"
[[patterns.repair_task.steps]]
name = "repair_task_agent"
type = "agent"
prompt = "{{task.prompt}}"
[patterns.repair_task.steps.text]
POSITION_CLAUSE = """
A previous agent attempted this task and left the tree red against the task's own gate. Repair its
work rather than starting over, and verify the repair before you finish.
"""
# The last step of the repair arm, deliberately unrouted: a red here is an unrouted failed gate, so
# the body invocation fails and `fail = "continue"` on the `for` records it and moves on.
[[patterns.repair_task.steps]]
name = "repaired_task_gate"
type = "gate"
command = "{{task.gate}}"
Two task references put the work definition in task files. prompt = "{{task.prompt}}" passes the file's prose body to a fresh worker as its complete prompt, and command = "{{task.gate}}" runs the gate command the file carries. A task file carries TOML front matter in the shape the shipped write-map-roster prompt specifies:
+++ gate = "the command this task must pass" +++ The worker prompt for this one task.
The failure policy changed, in two places. First, fail = "continue" on the for: a failed body invocation is recorded and the run moves to the next task. Second, the repair arm's re-gate is its last step with no if after it — a red repaired_task_gate is then an unrouted failed gate, the unrouted failure for.fail handles. The if after task_gate has no else: a non-matching outcome continues to the next step, and there is none, so a green gate completes the task.
Now open the shipped map.toml next to this file. Its map_task has the same agent, gate, repair, and re-gate sequence. Map also writes and checks a roster, records per-task verdict files under run/verdicts/, stops after two failed tasks in a row, declares on_plan_change, and sets on_stop = "write_handover". The check_streak command stops after one failed task if both tail -n 2 and -ge 2 become 1.
What you just learned: the {{task.prompt}} and {{task.gate}} task references and the task-file front-matter shape; command on a gate step; fail = "continue" and what counts as an unrouted failure it handles. Spec: § 9 for, § 6.2 gate, § 5 Names And References.
Stage 4 — a simple goal loop
The last shape: instead of building a fixed plan, work until an executable acceptance check passes. The format has no loop word — the only unbounded repetition is a pattern tail-calling itself — so the loop is: run the check; if it fails, plan the gap, build that plan with stage 2's split, and call the pattern again. Save as until-done.toml:
# ## until_done — recurse until an acceptance check passes
[header]
pattern_language_compatibility = "3"
entry = "until_done"
include = ["split.toml"]
description = """
Work toward a goal expressed as an executable acceptance check: run the check, and while it fails,
plan the gap it reports, build that plan through `split`, and start over. Ends when the check
passes, or when the gap planner reports no actionable work left.
"""
[patterns.until_done]
[[patterns.until_done.parameters]]
name = "until_source"
kind = "path"
declared_plan_source = true
description = "The goal prose the gap planner reads, and the run's identity."
[[patterns.until_done.parameters]]
name = "check"
kind = "path"
default = "bin/goal-check"
description = "The acceptance check record path: exit zero is achieved and non-zero is a gap."
[[patterns.until_done.steps]]
name = "run_check"
type = "command"
run = 'mkdir -p "$orchestration/state"; "$orchestration/$check" > "$orchestration/state/gap-report.txt" 2>&1'
outputs = [
{ name = "gap_report", path = "state/gap-report.txt" },
]
# Exit zero means the check passed: execution continues past this `if`, falls off the end of the
# entry pattern, and the run completes. Non-zero is the command's failed outcome, routed to the gap.
[[patterns.until_done.steps]]
if = { run_check = "failed" }
then = "close_gap"
[[patterns.close_gap.steps]]
name = "plan_gap"
type = "agent"
prompt = "write-gap-plan"
inputs = ["{{until_source}}", "{{gap_report}}"]
outputs = [
{ name = "gap_plan", path = "state/gap-plan.md" },
{ name = "gap_scope", path = "state/gap-plan-scope.txt", values = ["work", "none"] },
]
[[patterns.close_gap.steps]]
if = "gap_scope"
then = { work = "build_gap", none = "stop_no_gap" }
else = "stop_missing_gap_scope"
[[patterns.build_gap.steps]]
name = "build_gap_plan"
type = "call"
pattern = "split"
[patterns.build_gap.steps.parameters]
plan_source = "{{gap_plan}}"
# The tail call: the last step on every path through the cycle, which is what makes the recursion
# legal. There is no loop word and no cycle counter; the recursion ends when `run_check` succeeds or
# the planner declares `none`.
[[patterns.build_gap.steps]]
name = "next_cycle"
type = "call"
pattern = "until_done"
[patterns.build_gap.steps.parameters]
until_source = "{{until_source}}"
check = "{{check}}"
[[patterns.stop_no_gap.steps]]
type = "stop"
note = "The acceptance check is still red and the gap planner found no actionable work left."
[[patterns.stop_missing_gap_scope.steps]]
type = "stop"
note = "The gap planner finished without writing gap_scope, so there is no honest answer to whether work remains; the run stops rather than building a plan nobody vouched for."
include = ["split.toml"] composes Stage 2's definitions into this file's namespace. include entries resolve through the project tier, global mirror, then bundled tier, not relative to the including file's directory. Put both files in ~/.config/gantry/patterns/; a sibling file in another directory is not found. Step and output names must be unique across the composed definition, and the included file's header is ignored: only the entry file's header configures the run. build_gap_plan invokes Stage 2's entry with type = "call", binding plan_source to the gap plan.
The check is a command step: deterministic process text, the format's replacement for every predicate. The command receives its data as environment variables — the check parameter as $check, the orchestration directory as $orchestration; {{...}} never substitutes into command text — and its stdout writes the declared gap_report output. Control flow reads the step's outcome: if = { run_check = "failed" } routes a non-zero exit to close_gap, and a zero exit completes the run. A command in a deciding position either routes on its outcome or writes a one-word values output for an if; it never branches on stdout text directly.
The loop is next_cycle, a call of until_done from inside until_done. Recursion is the format's only unbounded repetition: there is no repeat, until, retry counter, or max_cycles key. An operator bounds a recursive run with lifecycle controls or an entry parameter. The loader requires every call participating in a cycle to be in tail position — no step in the caller may run after it returns — so next_cycle is the last step of build_gap; a non-tail recursive call is a load error. Each recursive invocation gets fresh per-invocation state, so cycle one's gap_plan is not readable in cycle two.
The loop ends three ways: run_check succeeds and execution falls off the end; the gap planner declares none and reaches stop_no_gap; or the planner writes no scope at all and reaches stop_missing_gap_scope.
The shipped pattern of this shape is until (config/patterns/until.toml). Its header claims the CLI selector flag until through [[header.cli_flags]], so gantry --until goal.md and gantry --pattern until goal.md are the same bound invocation. The shipped file is this stage plus refinements: it probes for the acceptance check with a command and, when absent, has a read_only agent author it from the goal prose; it protects the check from the agents that build against it; it accepts --param max_cycles <n> as an ordinary entry parameter whose command-authored branch stops after that many completed build cycles; and it builds each gap plan through the full build/milestone/sprint stack instead of split. This tutorial's version assumes the check already exists.
Spec: § 6.3 command, § 10 call, § 11 Recursion, § 3 Header.
Where to go next
- The shipped corpus (orchestration patterns):
build.toml→milestone.toml→sprint.tomlis the default milestone build (the stage-2 shape, elaborated, with recovery built from a sharedtroubleshootpattern and recursive retries),map.tomlis stage 3, anduntil.tomlis stage 4. Every file carries comments explaining its policy choices. - The authoring guide is the narrative authoring guide — how to think while writing a pattern, idiom by idiom.
- The pattern format is the normative reference: every table, key, and closed vocabulary, and what the loader rejects. When this tutorial and the spec disagree, the spec wins.
- Fork a shipped pattern. The bundled patterns are materialized into the global mirror
~/.config/gantry/patterns/; copy the nearest one under a new name and run it withgantry --pattern yours plan.md, or by file path withgantry --pattern ./yours.toml plan.md. A project-local copy at.gantry/<plan>/patterns/<name>.tomloverrides a same-named global fork, which overrides the bundled tier;gantry triage <plan>shows the registry with each pattern's tier and description. Edit a mirrored copy to make it a global override;gantry config resetrestores the shipped text.
docs/patterns/tutorial.md in the Gantry repository, rendered as it stands