Skip to main content

Saga manifests

A saga manifest is your workflow blueprint. Warden tracks each one by namespace, name, and version — see Component identity. It composes catalog step manifests into an ordered run, binds input ports, and gates steps with when. YAML examples below follow the GitHub MCP demo shape unless noted otherwise.

Capability fields (worker pin, prompt, tools, policy, hitl, facts, budgets, and so on) live on step manifests. This page covers saga composition and the runtime behavior of hydrated reason/commit steps.

Composing catalog steps​

Authoring YAML uses use: + version to pin a step definition in the saga's namespace. Bind ports with with, skip with when, and optionally apply tighten-only overrides (timeout_seconds, max_turns, HITL fields, token caps) that may only narrow catalog guardrails — never widen them.

At deploy, the engine link-checks every use: against the catalog (ports, tighten-only overrides, workers, artifacts) and stores the authoring graph on the saga definition. Running instances hydrate that graph into frozen_steps at start; they do not re-resolve the catalog at schedule time.

# config/saga.github-demo.yaml
kind: saga
name: github-demo
namespace: default
version: "0.1.0"
description: Triage open issues; post a governed comment when there is work.
steps:
- id: triage
use: github-triage
version: "0.1.0"
with:
owner:
from: $.input.owner
repo:
from: $.input.repo
focus_issue_number:
from: $.input.issue_number

- id: post-comment
use: github-post-comment
version: "0.1.0"
when:
cel: "has(steps.triage.facts.triage_metrics) && steps.triage.facts.triage_metrics.total_count > 0"
with:
owner:
from: $.input.owner
repo:
from: $.input.repo
issue_number:
from: $.steps.triage.output.data.recommended_issue_number
body:
from: $.steps.triage.output.data.comment_body
Saga-local fieldRole
idStable id in saga context, CEL, and bindings (steps.triage, …)
use / versionPin a step definition
withBind values into the step's declared inputs
whenOptional CEL skip — see Conditional branching
Tighten-only overridesNarrow catalog timeout_seconds, max_turns, HITL, token caps

Deploy workers → steps → sagas. See Step manifests for capability authoring and Manifests and artifacts → Deploy order.

Even though individual steps can be skipped, paused, or failed by human reviews and conditional logic, the hydrated structure of your workflow stays stable: forward order, which worker handles which step, and the rule that commit steps handle exactly one tool call. The sections below document that runtime shape.

Step kinds​

Every hydrated step is either reason or commit (from the catalog step_kind). Each needs a unique id and a name (from the saga ref or the step definition title):

FieldRole
idStable identifier in saga context, CEL, and with bindings (steps.triage, $.steps.triage.output…)
nameHuman-readable label for operators and logs

A reason step sends work to an LLM-backed worker to produce structured JSON output. By default, this uses agent-adapter: react, which lets the agent loop through multiple tool calls until it achieves its goal and invokes the built-in _submit tool. If you don't need a tool loop and just want a single, direct response from the model, set agent-adapter: simple instead on the step manifest. See Reason step execution (agent-adapter). Requires worker, worker_version, and prompt on the catalog step.

A commit step makes one deterministic MCP call with no LLM loop. Use it for side effects — posting a comment, triggering a webhook, writing a record. It requires worker and worker_version only (no prompt) on the catalog step.

After start hydrate, the instance frozen_steps look like inline reason/commit fields (illustrative — do not author this as saga YAML):

# Instance frozen_steps after start hydrate — not saga definition YAML
steps:
- id: triage
name: Triage open issues
kind: reason
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2

- id: post-comment
name: Post triage comment
kind: commit
worker: github-demo-worker
worker_version: "0.1.0"

Reason step execution (agent-adapter)​

Reason steps choose how the worker completes the step — separate from the worker manifest's adapter field (usually langchain).

YAML fieldWhereMeaning
adapter: langchainWorker manifestWhich agent runtime implementation the worker loads
agent-adapter: react | simpleReason step in step manifestExecution strategy inside that port

Decision matrix​

Use caseStep kindagent-adaptertools.allowHow output is produced
Tool-heavy agents (GitHub demo)reasonreact (default)MCP tool idsReAct loop → virtual _submit
Connectivity / single-turn transforms (Quickstart)reasonsimplemust be []Single structured LLM turn → JSON
Deterministic side effectcommit—exactly one toolMCP only, no LLM

react (default)​

Multi-turn ReAct loop. The worker binds MCP tools from tools.allow plus a virtual _submit tool (never list _submit in the allowlist). The model calls tools until it invokes _submit with a non-empty JSON object. Optional output_schema validates that payload; without it, any non-empty JSON shape is accepted.

_submit must be alone in its turn. If the model batches _submit with other tools, Warden runs the non-submit tools, returns a tool-role error for _submit (must be the only tool call), and continues the loop so the model can observe those results before submitting.

Warden admits sloppy LLM JSON against MCP tool inputSchema values and against _submit output_schema (stringified arrays/objects, scalar strings to numbers/booleans) before strict validation. See Configuration → LLM JSON admission.

simple​

Single structured LLM completion — no ReAct loop, no virtual _submit, no MCP tools. Warden rejects the deploy if you set agent-adapter: simple with non-empty tools.allow, resources.allow, or facts:.

When output_schema is omitted, the worker applies a built-in fallback schema requiring a summary string (steps.<id>.output.data.summary). Set output_schema when downstream bindings need stable field names. Structured payloads also go through LLM JSON admission before validation.

# config/step.minimal-step1.yaml — live inference smoke test (catalog)
kind: step
name: minimal-step1
version: "0.0.1"
step_kind: reason
agent-adapter: simple
worker: minimal-worker
worker_version: "1.0.0"
prompt: noop.j2
tools:
allow: []

Contrast with the GitHub triage step (default react, tools + _submit):

# config/step.github-triage.yaml (excerpt)
kind: step
name: github-triage
step_kind: reason
# agent-adapter: react # default — omit in YAML
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2
output_schema: github-triage-output.json
tools:
allow:
- name: list_issues
- name: issue_read

Failure codes by strategy​

agent-adapterTypical error_detailsMeaning
reactno_submit_callModel finished with text only — no _submit (after optional soft text-exit retry; see WARDEN_REACT_SUBMIT_TEXT_RETRIES)
reactempty_submit_result_submit called with {}
simplestructured_output_failedModel response was not parseable JSON (common on weak local models)
simpleempty_structured_resultParsed JSON object was empty
eitherSTEP_TOKEN_LIMIT_EXCEEDEDAccumulated provider total_tokens exceeded max_step_tokens (or WARDEN_MAX_STEP_TOKENS)
eithervalidation: output_schema / OUTPUT_SCHEMA_VALIDATION_FAILEDPayload failed JSON Schema

Optional context fields on error_details (CLI warden list steps --errors / warden show step):

FieldWhen present
reason, turns_used, last_assistant_contentno_submit_call when the model exited with prose instead of _submit (reason: model_text_exit)
reason, turns_used, last_tool_errorsno_submit_call when tool output matched MCP failure heuristics (e.g. MCP error: …)
tokens_used, max_step_tokens, prompt_tokens, completion_tokensSTEP_TOKEN_LIMIT_EXCEEDED
tool_result_previewFACT_EXTRACTION_FAILED / TOOL_RESULT_TRUNCATED when tool text explains the failure
truncation_limitTOOL_RESULT_TRUNCATED when JSON was cut at the worker record limit
response_previewstructured_output_failed on simple steps
messageAll normalized failures — human-readable summary

Persistence and redeploy​

When you start a saga, Warden saves the chosen agent_adapter on each step row. Deploy a new manifest version and start a fresh run to pick up a different strategy — running sagas keep what they started with.

Connecting workers to steps​

Each step definition points at a worker with worker (name) and worker_version. Steps don't declare their own namespace — Warden uses the parent saga's namespace to look up both the catalog step and (namespace, worker, worker_version). Cross-namespace references fail when you deploy. Deploy workers before steps before sagas: Manifests and artifacts → Deploy order matters.

Later sections describe runtime behavior of those fields. Author capability on step manifests; author with / when / tighten-only on this page's composition model. Worker manifests define LLM and MCP config — see Worker manifests.

Tool allowlists​

tools.allow lists the MCP tools a step may call. Author it on the step manifest; after start hydrate it is frozen on the instance. The worker rejects any tool not on the list.

Step kindAllowlist rule
reasonZero or more tools — list every tool the agent is allowed to use
commitExactly one tool — the side effect to execute

The worker manifest declares which MCP servers exist (tool_sources on the worker definition). The step definition declares what this step may use. When the step runs, the worker connects to those sources, discovers tool ids from the MCP server, and loads only the names in tools.allow. Execution strategy (react vs simple) is set per reason step — see Reason step execution (agent-adapter).

On a commit step, there is no agent loop. The worker calls the one allowed tool directly, using arguments from the saga ref's with bindings.

Names must match MCP tool ids exactly. If a listed tool is not exposed by the connected server, the step fails at runtime — Warden does not probe MCP connectivity when you deploy. See Worker manifests for tool_sources and MCP and tools for the full execution model.

# config/step.github-triage.yaml (excerpt)
kind: step
name: github-triage
step_kind: reason
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2
tools:
allow:
- name: list_issues
- name: issue_read
# config/step.github-post-comment.yaml (excerpt)
kind: step
name: github-post-comment
step_kind: commit
worker: github-demo-worker
worker_version: "0.1.0"
tools:
allow:
- name: add_issue_comment

Resource allowlists (resources.allow)​

Optional on react reason step manifests when the agent needs read-only MCP context (policy text, profile records) before or during tool calls. Incompatible with agent-adapter: simple. Commit steps have no ReAct loop, so resources.allow is rarely useful there.

Each entry under resources.allow requires a uri (and optional description). URI templates may include {placeholders} — the worker binds each placeholder to a resolved with value when the agent calls the virtual read_resource tool. Do not list read_resource in tools.allow; the worker injects it when this block is non-empty.

# Capability on the step; `with` stays on the saga `use:` ref
kind: step
name: review-risk
step_kind: reason
worker: risk-worker
worker_version: "1.0.0"
prompt: review.j2
inputs:
customer_id:
required: true
resources:
allow:
- uri: "file:///policies/fraud-v3.md"
- uri: "postgres://risk/profiles/{customer_id}"
tools:
allow:
- name: score_transaction

The worker must have MCP tool_sources — resource reads go through connected MCP servers, not arbitrary filesystem paths on the engine host. For traversal rules, parameterized URI matching, and how read_resource fits the ReAct loop, see MCP and tools → Resource allowlists.

Bindings (with)​

Add a with block on the saga use: ref when a step needs data from the saga's start input or from steps that already finished. Keys must match the catalog step's declared inputs. Resolved values are used differently per step kind — see the table below.

Warden resolves your with blocks right before it kicks off the step. It uses standard JSONPath syntax ($.…) to grab data from the saga's initial input or any outputs saved by earlier steps, and passes that combined context into the new step. Use from for JSONPath lookups, or value for a literal.

Step kindHow resolved with is used
reason (react)Every key renders the Jinja prompt. Optionally list a subset under tools.bind on the step manifest to also pin those values onto matching MCP tool args (saga wins; keys are stripped from the LLM-facing tool schema).
reason (simple)Prompt variables only. tools.bind is rejected.
commitThe full map becomes the single MCP tool's kwargs. Do not set tools.bind.
compensationSame as commit for the undo tool. Do not set tools.bind.
# Saga composition — config/saga.github-demo.yaml (excerpt)
steps:
- id: triage
use: github-triage
version: "0.1.0"
with:
owner:
from: $.input.owner
repo:
from: $.input.repo

- id: post-comment
use: github-post-comment
version: "0.1.0"
with:
body:
from: $.steps.triage.output.data.comment_body
# owner, repo, issue_number — see full demo saga

tools.bind example (capability on the step; values from the saga with):

# Step catalog — pin which with-keys overlay MCP args
kind: step
name: run-in-sandbox
step_kind: reason
worker: sandbox-worker
worker_version: "1.0.0"
prompt: sandbox-exec.j2
inputs:
container_id:
required: true
problem_statement:
required: true
tools:
bind:
- container_id
allow:
- name: sandbox_exec
# Saga ref — supply those ports
- id: run-in-sandbox
use: run-in-sandbox
version: "1.0.0"
with:
container_id:
from: $.steps.init-sandbox.output.data.container_id
problem_statement:
from: $.input.problem_statement

Pinning MCP args on reason steps (tools.bind)​

On react reason steps, the model normally chooses every MCP argument. Session keys such as container_id often get garbled when the LLM re-emits them. Declare those keys under tools.bind (each name must also appear in with):

  1. Warden overlays the resolved saga value onto the tool invoke for any key present in that tool's inputSchema.properties (saga always wins, including over empty/junk model args).
  2. Bound properties are omitted from the LLM-facing args_schema / required, so the model is not asked to invent them.
  3. Keys that are not on a given tool's schema stay prompt-only (no auto-intersect of all with keys).

tools.bind is not supported on agent-adapter: simple, commit steps, or compensation undo YAML.

What you can fetch​

Warden evaluates bindings right before the step runs, against the saga's context object. JSONPath expressions must start with $.

SourceJSONPathWhat you get
Saga start input$.input.<field>A field from the JSON passed to warden start saga --input
Prior step result$.steps.<step_id>.output.data.<field>Structured JSON from a completed step — reason steps (react or simple), MCP JSON on commit steps
Prior step tool facts$.steps.<step_id>.facts.<into>.<field>A value extracted from an MCP tool result on an earlier reason step — only when that step declared facts: and the tool ran. See Tool facts for how tool, into, and fields are declared.
Literalvalue: <any>A fixed value — no JSONPath lookup

Reason → commit boundary​

When a reason step finishes, the worker sends a STEP_COMPLETED envelope back to Warden — typically { "data": { … }, "facts": { … }? }. Warden validates output_schema against the inner data object, runs after_reason policy if you configured one, then saves:

Stored whereContents
saga_step_instances.output_payloadNormalized envelope (data + optional facts)
saga.context.steps.<step_id>Same shape: output.data and facts for JSONPath / with bindings

The ReAct message history (system, human, assistant, tool turns) is not copied into saga context. Downstream steps — including commit — only see what you bind explicitly from output, facts, or input.

When a commit step is about to run, Warden resolves its with block against the current saga context before dispatching the MCP call. That flat map becomes:

  • saga_step_instances.resolved_arguments on the commit step row
  • arguments on the worker command (single MCP tool invoke — no LLM loop)
- id: post-comment
use: github-post-comment
version: "0.1.0"
with:
body:
from: $.steps.triage.output.data.comment_body # structured reason output only

Custom AgentAdapterPort implementations must emit the same envelope on success; Warden never forwards raw adapter-internal state across steps. If you need a field at commit time, expose it in output.data or facts, then bind it in with.

Examples using triage → post-comment:

with:
owner:
from: $.input.owner
summary:
from: $.steps.triage.output.data.summary
open_count:
from: $.steps.triage.facts.triage_metrics.total_count
priority:
value: high

Only bind from steps that have already finished in forward order. When a saga starts, Warden pre-initializes every step id with empty output.data and facts, so a path to a future or incomplete step resolves to {} or null. Tool fact paths stay absent until the extractor's tool actually ran — use when.cel with has(...) for optional branches instead of relying on bindings alone. To populate facts buckets on a reason step, see Tool facts.

Once resolved, with values feed the step at run time. On commit steps they become MCP tool arguments. On reason steps they render the Jinja prompt; with tools.bind on react, selected keys also pin MCP args — see Prompts for how bindings become template variables and what Warden checks when you deploy.

Context scoping

Saga context is append-only per step id. When a step completes, Warden merges its output under steps.<step_id> — it does not mutate other steps' buckets. A later step cannot change what an earlier step stored.

Policies​

Add policy: <path> on the step definition — a path relative to POLICIES_ROOT with extension (e.g. github-issue-comment.yaml or teams/marketing/gate.yaml). Warden loads and validates the file when you deploy the step, then evaluates it at the gate. See Policies for file format, CEL binding, phases, and outcomes.

# config/step.github-post-comment.yaml (excerpt)
kind: step
name: github-post-comment
step_kind: commit
worker: github-demo-worker
worker_version: "0.1.0"
policy: github-issue-comment.yaml
tools:
allow:
- name: add_issue_comment

Walkthrough with before_commit CEL and HITL: GitHub MCP demo.

If a policy denies a step or evaluation errors, Warden marks the step FAILED and stops forward progress. Depending on how far the saga got, it may flip to COMPENSATING and run your declared undo steps backward (LIFO). See Compensation.

Human-in-the-Loop (HITL)​

Add hitl: true on the step definition to pause for operator review (sagas may tighten HITL further, never clear catalog hitl: true). Warden sets the saga to AWAITING_HUMAN until someone approves, rejects, or retries. If the step also has a policy, the policy gate runs first — HITL only applies when the policy passes. See Policies.

The hold point depends on step kind:

Step kindWhen the pause happensWhat the reviewer sees
reasonAfter the worker returns structured outputThe validated reason-step payload in output.data (editable on approve)
commitBefore the MCP tool is calledThe resolved with arguments — the side effect has not run yet

On post-comment:

# config/step.github-post-comment.yaml (excerpt)
kind: step
name: github-post-comment
step_kind: commit
worker: github-demo-worker
worker_version: "0.1.0"
hitl: true
# optional: hitl_max_retries / hitl_retry_guidance on the step or as tighten-only on the saga ref
tools:
allow:
- name: add_issue_comment
# Saga — wiring only
- id: post-comment
use: github-post-comment
version: "0.1.0"
with:
body:
from: $.steps.triage.output.data.comment_body

Optional retry limits while the step is held (on the step, or tighten-only on the saga use: ref):

hitl_max_retries: 2
hitl_retry_guidance: "Tighten the comment and cite the issue number."

hitl_max_retries caps how many times an operator may call warden review retry (omit for unlimited). hitl_retry_guidance is default text merged into the worker run as _hitl_retry.guidance; per-request --guidance on the CLI overrides it.

Operator actions (via warden review or the human-gate HTTP API):

ActionEffect
ApproveSaga resumes — context merges on reason steps; commit tool dispatches on commit steps
RejectStep fails; Warden runs compensation on completed forward steps (LIFO)
RetryRe-runs the worker/LLM while still AWAITING_HUMAN (reason steps only in practice; respects hitl_max_retries)

There is no built-in reviewer UI — the kernel exposes CLI and HTTP only. For commands, API paths, and async outbox behavior, see HITL review. End-to-end example: GitHub MCP demo.

Step budgets​

reason steps accept independent caps:

FieldApplies toDefaultMeaning
max_turnsreact only25 (max 200)Cap on back-and-forth tool/LLM rounds. simple ignores it (always one LLM call).
max_step_tokensreact and simpleunlimited (omit / null)Financial guardrail: abort when accumulated provider-reported total_tokens (prompt + completion across the step) exceed this budget.
max_completion_tokensreact and simpleno Warden override (omit / null)Per-call generation cap passed to the provider as max_tokens. Distinct from max_step_tokens.

max_step_tokens counts gross physical tokens from the provider usage metadata — not cache-discounted billed tokens. Prompt caching can make the invoice much smaller than the counted total; the budget still uses the raw counter. Token budgets apply to reason steps only; compensation undo is a single MCP call and does not use LLM budgets.

max_completion_tokens limits how much the model may generate on each LLM call (every ReAct turn shares the same cap). Omit it to leave the provider default (Anthropic via LangChain may still default to 8192).

Optional process-wide fallbacks: set worker env WARDEN_MAX_STEP_TOKENS / WARDEN_MAX_COMPLETION_TOKENS (see Configuration). Each applies only when the step omits the matching field. Unset or 0 means no fallback.

When the step budget is exceeded, the step fails with error_details.code: STEP_TOKEN_LIMIT_EXCEEDED (includes tokens_used, max_step_tokens, prompt_tokens, completion_tokens). Usage from completed LLM turns is still written to execution_usage on STEP_FAILED.

timeout_seconds is a safety clock for step execution (default 600 seconds). If a worker claims a step and then crashes or hangs, Warden waits for this window to expire, marks the step FAILED, and can trigger compensation — it won't auto-retry a stuck step. See Saga recovery for how the open kernel vs enterprise handle timeouts and stale claims.

On triage (catalog budgets; saga refs may only tighten these):

# config/step.github-triage.yaml (excerpt)
kind: step
name: github-triage
step_kind: reason
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2
max_turns: 15
timeout_seconds: 600
# optional: max_step_tokens / max_completion_tokens
tools:
allow:
- name: list_issues
- name: issue_read

Compensation undo is always a single deterministic MCP call (exactly one tools.allow entry). See Compensation.

Structured output (output_schema)​

Reason steps can require a fixed JSON shape for worker output in output.data. Set output_schema to the schema filename (relative to SCHEMAS_ROOT) — a JSON Schema .json file at config/<schema-file>.json.

agent-adapterWhat gets validated
react_submit payload after the ReAct loop
simpleStructured completion from the single LLM turn

Supported schema subset​

Warden binds output_schema for simple structured output via a Pydantic subset, then validates with JSON Schema for both adapters. Supported structural pieces:

  • type: object, string, integer, number, boolean, array, and nullable unions like ["string","null"]
  • properties, required, items, description
  • Validation-only constraints such as minLength, enum, minimum
  • additionalProperties: false — also applied at bind (extra="forbid" on the Pydantic model for simple / _submit); omit it or set true and bind allows extra keys (JSON Schema gate still follows the keyword)

Rejected at deploy / saga start (silent no-ops at the structured-output binder): if, then, else, allOf, anyOf, oneOf, $ref, $defs, definitions. Flatten conditionals into always-required fields or split steps; inline $ref targets.

On triage (react):

# config/step.github-triage.yaml (excerpt)
kind: step
name: github-triage
step_kind: reason
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2
output_schema: github-triage-output.json
tools:
allow:
- name: list_issues
- name: issue_read

When you start a saga, Warden resolves config/<schema-file>.json, freezes the object onto instance frozen_steps as output_schema_definition, and copies it onto the step row output_schema. Loop mint and materialize read that embed. The worker validates output before sending STEP_COMPLETED. Warden validates again when it ingests that completion event.

Without output_schema: react still requires a non-empty _submit payload (any JSON shape); simple uses the built-in fallback requiring summary.

When you need a schema

Omitting output_schema is fine for single-step smoke tests on simple. For anything that chains steps, treat a schema as the contract between the agent and saga context.

Downstream steps read prior results through paths like $.steps.triage.output.data.comment_body in with bindings, when.cel, and policy gates. HITL on a reason step exposes the same structured output.data object for review. If field names or types drift between runs, those bindings resolve to {} or null, or fail when the step schedules. A JSON Schema file fixes the shape Warden validates before the output lands in context.

Use tool facts when you need structured data from MCP tool JSON instead of from reason-step output (react only).

Outcomereactsimple
Valid structured JSONProceeds (policy, optional HITL, context merge)Same
Missing / empty outputno_submit_call / empty_submit_resultstructured_output_failed / empty_structured_result
Schema mismatchSoft-retry with validation feedback (WARDEN_LLM_SCHEMA_RETRY_*), then STEP_FAILED if still invalidSame

max_turns bounds ReAct tool/LLM iterations on react only. Schema soft-retries (WARDEN_LLM_SCHEMA_RETRY_*) are a separate attempt count, but on react each retry still consumes remaining max_turns from the same step budget — see Configuration → LLM schema soft-retries.

Commit steps can also attach output_schema for tool result validation.

Conditional steps (when)​

Optional when.cel on a forward saga use: ref runs before Warden schedules the step. false skips the step; a runtime evaluation error fails it with WHEN_EVALUATION_FAILED. Syntax, CEL bindings, examples, and troubleshooting: Conditional branching (when.cel).

- id: post-comment
use: github-post-comment
version: "0.1.0"
when:
cel: "has(steps.triage.facts.triage_metrics) && steps.triage.facts.triage_metrics.total_count > 0"

Policy gates use a different CEL binding (no steps root) — see Policies.

Tool facts (facts)​

On react reason steps only (agent-adapter: simple rejects facts: when you deploy), facts copies selected values out of MCP tool JSON into saga context after the ReAct loop finishes. Extraction uses whatever the agent actually called during the step — not the structured reason-step output in output.data. The same full tool payload is stored in tool_results and sent to the LLM transcript at record time — see MCP and tools → Tool payload hygiene.

Each extractor has three parts:

KeyWhat it isWhat it does
toolMCP tool idWhich tool call to read from. Must be the original MCP tool id (not the sanitized LLM wire name) and match a call the agent made during this step. If the agent never called that tool, this extractor is skipped entirely. tools.allow may list either the raw MCP id or its sanitized form.
intoBucket name you chooseGroups the extracted fields under steps.<step_id>.facts.<into>. Use a short, stable id (e.g. triage_metrics) — this is your saga-context name, not the MCP tool name.
fieldsMap of saga key → JSONPathFor each entry, the left key is the name you use in when.cel and with (total_count). The right value is JSONPath into the tool's JSON response ($.totalCount).
MCP tool JSON (list_issues) Manifest facts block Saga context fragment
──────────────────────────── ──────────────────────── ───────────────────────────────────
{ - tool: list_issues steps.triage.facts.triage_metrics
"totalCount": 3, into: triage_metrics .total_count = 3
"issues": [...] fields:
} total_count: "$.totalCount"

Walkthrough for triage (facts on the step catalog):

# config/step.github-triage.yaml (excerpt)
facts:
- tool: list_issues
into: triage_metrics
fields:
total_count: "$.totalCount"
  1. During triage, the agent calls the list_issues MCP tool (allowed in tools.allow).
  2. The tool returns JSON — for example {"totalCount": 3, "issues": [...]}.
  3. The worker runs $.totalCount on that JSON and stores the result as total_count.
  4. Saga context gets steps.triage.facts.triage_metrics.total_count == 3.
  5. A later step can gate on it: when.cel: "has(steps.triage.facts.triage_metrics) && steps.triage.facts.triage_metrics.total_count > 0".

MCP tool response (list_issues return value):

{
"totalCount": 3,
"issues": [
{"number": 1, "title": "Example issue"}
]
}

Saga context fragment after extraction (simplified — output.data holds the reason-step _submit payload separately):

{
"steps": {
"triage": {
"output": { "data": { "summary": "..." } },
"facts": {
"triage_metrics": {
"total_count": 3
}
}
}
}
}

Use steps.triage.facts.triage_metrics.total_count in when.cel and with.from — not totalCount (raw tool JSON) or list_issues (tool id).

On triage (full step context):

# config/step.github-triage.yaml (excerpt)
kind: step
name: github-triage
step_kind: reason
worker: github-demo-worker
worker_version: "0.1.0"
prompt: github-triage.j2
tools:
allow:
- name: list_issues
- name: issue_read
facts:
- tool: list_issues
into: triage_metrics
fields:
total_count: "$.totalCount"
Three different names

tool: list_issues must match the MCP tool id. into: triage_metrics is your facts bucket in saga context — they need not match. In fields, the JSONPath reads the provider's JSON ($.totalCount); the field key is what you write in saga context (total_count). Use steps.triage.facts.triage_metrics.total_count in when and with, not totalCount or list_issues.

BehaviorResult
Tool never calledinto bucket omitted — no entry under steps.<id>.facts.<into>
Tool called, JSONPath matchesValues stored at steps.<id>.facts.<into>.<field>

If the tool runs but your JSONPath doesn't match anything in the response, Warden fails the step with FACT_EXTRACTION_FAILED.

End-to-end example: GitHub MCP demo.

What's next​

Every reasoning step needs a prompt template file to guide the agent. Head over to Prompts to write Jinja templates and map your with blocks into template variables.