Skip to main content

Conditional branching (when.cel)

when.cel is an optional CEL expression on a saga step. Before Warden schedules a step to run, it evaluates this expression. If the result is true, the step runs normally. If false, Warden skips the step (SKIPPED) and moves on to the next forward step in your manifest — the next blueprint step by order_index, not a compensation undo row. Use it for optional paths and guards on prior output or tool facts — not for blocking a commit after arguments are resolved (that is a Policy gate).

End-to-end example: GitHub MCP demo skips post-comment when the repo has no open issues.

Attaching to a saga step

Add a when block with a cel string on any forward step:

- id: post-comment
kind: commit
worker: github-demo-worker
worker_version: "1.0.0"
when:
cel: "has(steps.triage.facts.triage_metrics) && steps.triage.facts.triage_metrics.total_count > 0"
tools:
allow:
- name: add_issue_comment
ResultWhat happens
No when blockStep always runs when reached in manifest order
trueStep scheduled (worker or commit tool runs)
falseStep SKIPPED; engine advances to the next blueprint step by order_index
Expression error at runtimeStep FAILED with WHEN_EVALUATION_FAILED in status; saga may compensate

Invalid when.cel syntax is caught when you deploy the saga — deploy fails before any instance runs.

CEL evaluation context

At schedule time the engine exposes these top-level names in when.cel:

NameContents
inputSaga start payload (context.input)
stepsFull context.steps map — prior step output.data, facts, and pre-initialized empty buckets for steps not yet run
sagatrace_id, namespace, status
stepBlueprint step being scheduled: id (manifest id — CLI --step-id, keys under steps), name (manifest name — display label; often differs from id), kind, order_index

when.cel does not receive resolved with arguments for the current step, policy phase, or tool.name — those belong to policy CEL. To gate a commit on values from an earlier step, read them from steps.<prior_id>.output.data or steps.<prior_id>.facts.<into>.<field>, or bind them in the prior step's output and reference them here.

Reading prior steps

Only prior steps in forward order are meaningful. At saga start every step id is pre-initialized with {output: {data: {}}, facts: {}}. Paths like steps.triage.output.data.summary always resolve — to empty or null until that step completes, not an evaluation error. Optional facts buckets are different: if the extractor's tool never ran, the into bucket may be absent — use has(steps.<id>.facts.<into>) so a missing bucket becomes false (skip) instead of WHEN_EVALUATION_FAILED.

Tool facts and naming

Manifest facts.fields keys (e.g. total_count) become saga-context names. JSONPath values (e.g. "$.totalCount") read raw MCP tool JSON. CEL must use extracted names:

steps.triage.facts.triage_metrics.total_count ✓ (saga context)
$.totalCount ✗ (raw tool JSON — not in CEL binding)

Walkthrough: Saga manifests → Tool facts. Defensive patterns: Lifecycle → Step SKIPPED.

when.cel vs policy

Both use CEL, but they are different gates:

when.cel on a steppolicy: on a step
QuestionShould this step run at all?May this step proceed past the gate?
Evaluated whenBefore schedulingafter_reason or before_commit
If falseStep SKIPPED; saga advances to next blueprint step by order_indexStep FAILED; compensation if configured
steps in binding?Yes — full context.stepsNo — exposes phase, input, arguments, output, saga, step, worker, and tool only (Policies → CEL evaluation context); no steps

Policy CEL never receives steps.*. Gate on prior-step values via with into arguments (commit) or the current reason step's output (after_reason). Full policy reference: Policies.

Troubleshooting

SymptomLikely causeFix
Step FAILED; WHEN_EVALUATION_FAILED in error_detailsCEL referenced a path that does not exist or wrong type at runtimeUse has() for optional facts buckets; verify field names match manifest facts.fields keys, not raw tool JSON
Step SKIPPED unexpectedlyExpression returned falseIf your step is getting SKIPPED out of nowhere, your expression is likely returning false because of a missing piece of data. This usually happens if a prior step didn't output what you expected, a field name is misspelled, or you forgot to wrap an optional field in has(). Run warden show step to peek at what's actually sitting in context, and double-check your property paths against your manifest — see Tool facts for naming.
Deploy rejected on when.celSyntax or compile errorFix the CEL expression and redeploy; same compile path as policy CEL
warden list steps --trace-id <TRACE_ID> --errors
warden show step <TRACE_ID> --step-id <STEP_ID>

What's next

Next up: Policies — author deterministic runtime guardrails that validate model outputs and incoming tool parameters.