Patterns
Patterns
The runner derives execution layers from depends_on. If no step in the recipe
declares a dependency, steps run in file order, one after another. As soon as
one step declares a dependency, steps are grouped into layers by dependency
depth. Every step in a layer runs concurrently, and the next layer starts only
after the whole previous layer has finished. Everything below is a shape you
get from that one rule, and every transcript on this page came from running
the recipe.
Sequence
The default. Each step depends on the one before it, and the last step proves the artifact the first one produced.
{
"name": "hello-notes",
"type": "sequence",
"steps": [
{
"label": "write",
"backend": "shell",
"prompt": "mkdir -p notes && printf 'runner verified\n' > notes/hello.md && echo WRITE_COMPLETE",
"completion_keyword": "WRITE_COMPLETE",
"expected_changes": ["notes/hello.md"],
"timeout": 60
},
{
"label": "prove",
"backend": "shell",
"depends_on": ["write"],
"prompt": "echo PROVE_COMPLETE",
"completion_keyword": "PROVE_COMPLETE",
"verify": {
"grep_present": [
{ "file": "notes/hello.md", "pattern": "runner verified" }
]
},
"timeout": 60
}
]
}░░░░░░░░░░░░░░░░░░░░ 0% 0/2 steps
⏵ 1/2 write shell 0s running
WRITE_COMPLETE
✓ 1/2 write shell 0s ok
██████████░░░░░░░░░░ 50% 1/2 steps
⏵ 2/2 prove shell 0s running
PROVE_COMPLETE
✓ 2/2 prove shell 0s ok
✓ hello-notes · 2 step(s) · okFan out and join
Two steps with no dependency between them land in the same layer and run together. A third names both and cannot start until the layer closes.
{
"name": "fanout",
"type": "sequence",
"steps": [
{
"label": "probe-a",
"backend": "shell",
"prompt": "echo A_COMPLETE",
"completion_keyword": "A_COMPLETE",
"verify": { "git_clean_outside": ["notes/"] },
"timeout": 60
},
{
"label": "probe-b",
"backend": "shell",
"prompt": "echo B_COMPLETE",
"completion_keyword": "B_COMPLETE",
"verify": { "git_clean_outside": ["notes/"] },
"timeout": 60
},
{
"label": "join",
"backend": "shell",
"depends_on": ["probe-a", "probe-b"],
"prompt": "mkdir -p notes && printf 'both probes closed\n' > notes/summary.md && echo JOIN_COMPLETE",
"completion_keyword": "JOIN_COMPLETE",
"expected_changes": ["notes/summary.md"],
"timeout": 60
}
]
}░░░░░░░░░░░░░░░░░░░░ 0% 0/3 steps
⏵ 1/3 probe-a shell 0s running
⏵ 2/3 probe-b shell 0s running
A_COMPLETE
B_COMPLETE
✓ 1/3 probe-a shell 0s ok
✓ 2/3 probe-b shell 0s ok
█████████████░░░░░░░ 67% 2/3 steps
⏵ 3/3 join shell 0s running
JOIN_COMPLETE
✓ 3/3 join shell 0s ok
✓ fanout · 3 step(s) · okThe two probes started before either printed anything, and their output can
arrive in either order from one run to the next. Only the joining step
declares expected_changes, which is deliberate. See the caveat below.
Use --max-parallel N, or max_parallel in the recipe, to cap how wide a
layer runs.
Concurrent steps must not declare expected_changes
Steps in one layer share a git working tree, and each one commits when it closes. Two simultaneous committers race on the git index, and the loser's commit fails, which marks that step failed even though its work succeeded. A step can also see a neighbour's uncommitted write as its own drift and quarantine it. Three steps in one layer, each declaring its own file, produced this:
✗ 1/4 wa shell 0s failed · exit 0
✗ 2/4 wb shell 0s failed · exit 0
✓ 3/4 wc shell 0s ok · Quarantined changes outside expected_changes: r/b.mdGive concurrent steps verify.git_clean_outside instead. It bounds the writes
without committing anything, and a later serial step does the commit. That is
the shape the fan-out example above uses.
The runner does not give steps their own worktrees. Every step in a run works in the same checkout.
Compound
A compound recipe runs whole recipes as nodes, with depends_on between them.
Each child is an ordinary recipe file, runnable on its own.
{
"name": "pipeline-demo",
"type": "compound",
"steps": [],
"recipes": [
{ "label": "gather", "recipe": "collect" },
{ "label": "write-up", "recipe": "summarize", "depends_on": ["gather"] }
]
}░░░░░░░░░░░░░░░░░░░░ 0% 0/2 recipes
⏵ gather recipe 0s running
░░░░░░░░░░░░░░░░░░░░ 0% 0/1 steps
⏵ 1/1 collect shell 0s running
COLLECT_COMPLETE
✓ 1/1 collect shell 0s ok
✓ gather recipe 0s ok
██████████░░░░░░░░░░ 50% 1/2 recipes
⏵ write-up recipe 0s running
░░░░░░░░░░░░░░░░░░░░ 0% 0/1 steps
⏵ 1/1 summarize shell 0s running
SUMMARIZE_COMPLETE
✓ 1/1 summarize shell 0s ok
✓ write-up recipe 0s ok
✓ pipeline-demo · 2 step(s) · okThe empty steps key is required. A compound, pipeline, or parallel recipe
without it fails to load with a decoding error that does not name the field.
pipeline and parallel take the same recipes array and differ in how the
nodes are scheduled: compound honors depends_on as a graph, pipeline runs
the nodes one after another in file order, and parallel runs them all
together, capped by max_parallel. pipeline and parallel still refuse a
depends_on that names an unknown node, but they do not schedule by it.
Picking up a partial run
Long runs fail in the middle. Both recovery commands read the run record rather than starting over:
mentu-recipes resume <run-id>
mentu-recipes retry-step <run-id> <step-label>resume skips steps already marked success or warn_bookkeeping and reruns
the rest, appending to the existing events.jsonl. retry-step marks one
step pending and resumes from there, leaving completed dependencies alone.
Here a second step failed its verify.commands check, the missing file was
put in place, and the run was resumed:
⊘ 1/2 first already complete
██████████░░░░░░░░░░ 50% 1/2 steps
⏵ 2/2 second shell 0s running
SECOND_DONE
✓ 2/2 second shell 0s ok
✓ two · okWorked examples
Three runnable recipes are documented with full transcripts under
examples/ in the
repository. The recipe files themselves live in that repository's
.mentu/recipes/, and examples/run-demo.sh <name> copies them into a
throwaway git workspace under $TMPDIR and runs one there, so the commits the
runner makes stay out of your clone. The script runs the binary from a local
swift build by default, or the one named by MENTU_RECIPES_BIN. All three
use the shell backend and need no credentials.