Mentu

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 note no changes matched expected_changes.
  • Not a git worktree, or git not 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-key

cloud

{
  "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"
    }
  ]
}
© 2026 Mentu.