Recipe schema
Recipe schema
A recipe is a JSON object with a name and either a list of steps or a list of child recipes. This page is the field reference for the public runner. Anything not listed here is not read.
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Recipe name. Resolution is by this name. |
type |
string | no | sequence, formula, compound, pipeline, or parallel. Default is sequence. |
description |
string | no | Human-readable description. |
backend |
string | no | Default backend for steps that do not name one. |
model |
string | no | Default model for steps that do not name one. |
env |
object | no | Recipe-level environment values. |
providers |
object | no | Custom provider definitions. |
cloud |
object | no | Optional cloud block. Off by default. |
hooks |
object | no | Shell hooks around the run and around each step. |
max_parallel |
integer | no | Cap on concurrent steps or child recipes. --max-parallel on the command line overrides it. |
steps |
array | always present | Steps, ordered or dependency-linked. Must be non-empty for sequence and formula. |
recipes |
array | for compound, pipeline, and parallel |
Child recipe nodes. Must be non-empty for those types. |
The built-in backend names are shell, openai, openai-chat, deepseek,
ollama, claude, and codex. openai-responses and chatgpt are aliases
for openai. Names are matched case-insensitively, with _ treated as -.
A compound, pipeline, or parallel recipe must still carry a steps key,
even an empty one. Without it the file fails to load, and the decoding error
does not name the missing field:
mentu-recipes: Invalid recipe: .../no-steps.json: The data couldn’t be read because it is missing.Step fields
| Field | Type | Required | Description |
|---|---|---|---|
label |
string | yes | Unique step label. Used by depends_on, retry-step, and the run record. |
prompt |
string | one of the two | Inline prompt, or the shell command for a shell step. |
prompt_file |
string | one of the two | Prompt file name resolved from the prompt roots. |
backend |
string | no | Step backend. Overrides --backend, which overrides the recipe default. |
model |
string | no | Step model. Overrides --model, which overrides the recipe default. |
dir |
string | no | Step working directory, relative, inside the workspace. |
env |
object | no | Step-level environment values. |
timeout |
integer | no | Seconds. Default is 1800. |
completion_keyword |
string | no | Text that must appear in the step's stdout or stderr for the step to count as complete. |
depends_on |
array | no | Step labels that must close first. One depends_on anywhere switches the recipe to layered scheduling, described below. |
max_retries |
integer | no | Retries after the first attempt. Default is 0. |
retry_backoff_ms |
integer | no | Wait between attempts. Default is 1000. |
max_output_bytes |
integer | no | Output capture limit. Default is 5000000. |
expected_changes |
array | no | Repo-relative paths or globs the step may commit. |
verify |
object | no | Deterministic checks run after the step. |
reasoning |
string | no | Provider reasoning effort, where the backend supports it. |
thinking |
string | no | Agent CLI thinking option, where the backend supports it. |
max_output_tokens |
integer | no | Provider output cap, where the backend supports it. |
allowed_tools |
array | no | Agent CLI allow-list, where the backend supports it. |
disallowed_tools |
array | no | Agent CLI deny-list, where the backend supports it. |
Each step needs either prompt or prompt_file. Labels are 1 to 128
characters of letters, digits, dot, underscore, or dash, starting with a letter
or digit, and must be unique. A step that names an unknown step in
depends_on, or a dependency cycle, is refused at check time. Per-backend
option support is reported by mentu-recipes adapters --json, and doctor
reads it to flag options a backend will ignore.
A step is retried only when its completion signal is not met: a non-zero exit,
or a missing completion_keyword. Verification runs once, after the last
attempt, and a failed verification is not retried.
With no depends_on anywhere, steps run in file order, one at a time. With at
least one depends_on, the runner groups steps into layers by dependency
depth: steps with no dependencies form layer 0, steps whose dependencies are
all in layer 0 form layer 1, and so on. Steps in one layer run concurrently, up
to max_parallel. The next layer starts only after the whole previous layer
has finished. A step whose dependency failed is skipped.
Child recipe nodes
Used by compound, pipeline, and parallel.
| Field | Type | Required | Description |
|---|---|---|---|
recipe |
string | yes | Recipe name, or a path inside the recipe roots. |
label |
string | no | Node label. Defaults to recipe. |
depends_on |
array | no | Other node labels that must close first. Honored by compound only. |
vars |
object | no | Variables passed into the child recipe, layered over the parent's --var values. |
Each child runs as its own recipe with its own run directory and run id. The
parent run directory records the child's run id in <label>.child-run.txt.
expected_changes
{
"expected_changes": ["Sources/", "Tests/**/*.swift", "README.md"]
}Patterns are matched with fnmatch. A pattern with no wildcard also matches
everything under that directory. Absolute, home-rooted, and traversal entries
are ignored. Paths under .mentu/runs/ and .mentu/cache/ are the runner's
own and never count as drift.
When the step closes, matching dirty paths are staged and committed with the
message chore: mentu-recipes step <label> (<run-id>). Unrelated dirty paths
go to a quarantine patch under the run directory.
The check is baseline-aware: files that were already dirty before the step started are recorded as pre-existing and are not attributed to the step.
The outcome depends on what matched:
- Declared paths changed, nothing else:
success. - Declared paths changed and undeclared paths too: the undeclared ones are quarantined and the step is
warn_bookkeeping. - Only undeclared paths changed: they are quarantined and the step is
failed. - Nothing changed:
warn_bookkeeping, with the noteno changes matched expected_changes. - Not a git worktree, or
gitnot on the path:warn_bookkeeping, nothing is committed.
verify
{
"verify": {
"grep_present": [{ "file": "README.md", "pattern": "Mentu Recipes", "min": 1 }],
"grep_absent": [{ "file": "CHANGELOG.md", "pattern": "TODO", "description": "no TODO markers at release" }],
"file_absent": [{ "file": ".env" }],
"git_clean_outside": ["Sources/", "Tests/"],
"commands": ["swift test"]
}
}grep_present and grep_absent take file, pattern, and an optional
description that is printed when the assertion does not hold. pattern is
literal text, not a regular expression. grep_present also takes min, which
defaults to 1, and max. A missing file or an unmet pattern marks the step
warn_bookkeeping and the run continues. file_absent takes file and an
optional description. It, git_clean_outside, and commands fail the step.
git_clean_outside is baseline-aware in the same way as expected_changes:
only files the step dirtied can fail it. Verification file paths are relative
to the step directory and cannot escape it. Verification commands run through
/bin/sh from the step directory with a 300 second timeout.
hooks
{
"hooks": {
"before_run": ["echo starting"],
"before_step": ["echo step $MENTU_RECIPES_STEP"],
"after_step": ["echo done"],
"on_error": ["echo failed"],
"after_run": ["echo finished"]
}
}Hooks run through /bin/sh with a 120 second timeout. Run-level hooks run
from the workspace, step-level hooks from the step directory. after_step
runs when the step closed ok, on_error when it did not. A hook's exit code is
recorded but does not change the step's outcome. Each hook's stdout and stderr
land in the run directory as hook-<event>-<step>-<n>.stdout and .stderr,
and every hook invocation is listed in run.json.
Hooks see these variables:
| Variable | Value |
|---|---|
MENTU_RECIPES_HOOK_EVENT |
before_run, before_step, after_step, on_error, or after_run |
MENTU_RECIPES_RUN_ID |
The run id |
MENTU_RECIPES_RECIPE |
The recipe name |
MENTU_RECIPES_STEP |
The step label, empty for run-level hooks |
MENTU_RECIPES_BACKEND |
The step or recipe backend, if named |
MENTU_RECIPES_MODEL |
The step or recipe model, if named |
MENTU_RECIPES_WORKSPACE |
The directory the hook runs from |
MENTU_RECIPES_STATUS |
running, ok, or failed |
MENTU_RECIPES_STEP_STDOUT, MENTU_RECIPES_STEP_STDERR |
Paths to the step's captured output. after_step and on_error only. |
providers
{
"providers": {
"acme": {
"api": "chat_completions",
"base_url": "https://api.acme.example/v1",
"api_key_env": "ACME_API_KEY",
"api_key_vault": "acme-api-key",
"model": "acme-large"
}
}
}Supported api values are responses, chat_completions, cli, and shell.
api defaults to chat_completions and base_url to the OpenAI endpoint.
cli maps to the codex agent CLI when the provider is named codex and to
the claude agent CLI otherwise. shell maps to the shell backend.
Give a custom provider its own credential name: naming the built-in OpenAI or
DeepSeek one is a doctor error, and at run time the runner refuses to send
those credentials to any host other than their own. See
Credentials.
A provider step stops before it opens a connection when the credential is missing, and the message names both places it looked:
✗ 1/1 ask Backend unavailable: acme requires ACME_API_KEY or vault key acme-api-keycloud
{
"cloud": {
"enabled": true,
"evaluate_steps": false
}
}| Field | Type | Default | Meaning |
|---|---|---|---|
enabled |
boolean | false |
Same as passing --cloud on run. |
evaluate_steps |
boolean | false |
Also report each step when the hooks are active. |
The hooks post to https://api.mentu.ai and require a Mentu API key in
MENTU_API_KEY or in the vault as mentu-api-key or mentu-api-token. There
is no public way to obtain a key today, so for the public runner this block has
no effect: the run proceeds locally and run.json records cloud_mode as
local-only. See What leaves the machine for the modes.
Minimal recipe
{
"name": "shell-smoke",
"steps": [
{
"label": "say-hello",
"backend": "shell",
"prompt": "echo MENTU_RECIPES_COMPLETE",
"completion_keyword": "MENTU_RECIPES_COMPLETE"
}
]
}