Skip to main content

Policies

Policies are CEL rules the engine evaluates at fixed points in a step's lifecycle. They live as YAML files under POLICIES_ROOT and are referenced by path from saga steps (policy: my-gate.yaml), not by (namespace, name, version) like saga definitions.

Only the engine evaluates policies — never the worker. Unlike an LLM, CEL rules are entirely deterministic—the model can't hallucinate its way around them, interpret them creatively, or override your guardrails. The engine evaluates the exact same expression against the same inputs and gives you an identical pass/fail result every single time. The only thing that changes is when it checks: reasoning steps are evaluated right after the worker finishes (after_reason), while commit steps are evaluated right before a tool call leaves the engine (before_commit). On commit steps, before_commit is the hard gate — bad arguments never reach an external system. On react reason steps, MCP tools on the allowlist may already have run before after_reason fires; for a single irreversible side effect with a hard gate, use a commit step.

The sections below cover policy files, attachment syntax, CEL bindings, outcomes, and how policies differ from when.cel.

End-to-end example: GitHub MCP demo.

Policy files​

Each policy is a YAML file under POLICIES_ROOT (default ./config/policies in the repo). Paths like ./config/policies are relative to your repository root (where the engine runs)—not to this documentation file's location in the docs tree. The file must contain a non-empty cel: expression. Optional metadata:

FieldPurpose
nameDisplay name (defaults to the manifest ref minus extension, preserving subdirs)
versionOptional label on the policy artifact (frozen onto the step at saga start)
celCEL expression — must evaluate to bool (true = pass, false = deny)
# config/policies/github-issue-comment.yaml
name: github-issue-comment
version: "1"
cel: "phase == 'before_commit' && step.kind == 'commit' && step.id == 'post-comment' && tool.name == 'add_issue_comment' && arguments.owner == input.owner && arguments.repo == input.repo && arguments.issue_number > 0 && size(arguments.body) > 0 && size(arguments.body) <= 8000 && arguments.body.contains('## Warden triage')"

warden deploy validates that each referenced policy file exists and that its CEL compiles. At saga start, the engine freezes {name, version, cel} into the instance frozen_steps (and onto the step row as policy_definition), same pattern as compensation embeds. Runtime gates evaluate that frozen CEL. Keep policy files on disk where the engine can read them at start/deploy — see Configuration for POLICIES_ROOT and Compose mounts.

CEL expressions evaluate to true or false — in a policy gate, true passes and false denies. For syntax, operators, and functions, see the CEL documentation.

Attaching to a step​

Reference a policy by path on the step definition that should be gated (not on the saga use: ref):

# 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

Saga composition binds ports and optional when / HITL tighten overrides — see Step manifests and Saga manifests.

Evaluation phases​

The engine chooses the evaluation phase from the step's kind — you do not set after_reason or before_commit in the saga manifest or policy file.

PhaseWhenStep typeWhat CEL can inspect
after_reasonAfter the worker returns structured reason-step output, before it is merged into saga contextReasonoutput (validated business object), arguments (resolved step inputs)
before_commitBefore DO_COMMIT is queued to the workerCommitarguments (resolved with → MCP tool args), tool.name

On react reason steps, MCP tools on the allowlist may already have run during the ReAct loop before after_reason fires. For a single irreversible side effect with a hard gate, use a commit step and before_commit.

CEL evaluation context​

At gate time the engine exposes these top-level names in policy cel::

NameContents
phase"after_reason" or "before_commit"
inputSaga start payload (context.input)
argumentsResolved step inputs — on commit steps, the MCP argument map from with
outputReason-step structured output in output.data (empty on before_commit)
sagatrace_id, namespace, status
stepid, name, kind, order_index
workername (worker manifest name)
toolname of the single allowed tool (commit steps only)

Policy CEL does not receive steps.* from saga context. To gate on a prior step's output or facts, bind the values you need into with (commit) or read them from the current step's output / arguments (reason). The GitHub demo policy checks arguments.owner, arguments.body, etc., because with already resolved them from earlier steps.

Policy vs when.cel​

Both use CEL, but they are different gates with different bindings. Full when.cel reference: Conditional branching.

when.cel on a steppolicy: on a step
What does it ask?Should this step run at all?May this step proceed past the gate?
Evaluated when?Before the step is scheduledafter_reason or before_commit
If false?Step SKIPPED; saga continuesStep FAILED; compensation if needed
steps in binding?Yes — full context.stepsNo
Typical use?Branching, optional stepsValidate agent output or block bad commit args

Policy files are validated (load + CEL compile) at saga registration as well.

Outcomes​

CEL evaluates to true or false. true → passed; false → denied. If the expression cannot run (missing file, parse error, wrong type), the outcome is errored — treated like a denial for the step.

OutcomeWhenEffect
passedCEL is trueStep continues
deniedCEL is falseStep fails (POLICY_REASON_DENIED or POLICY_COMMIT_DENIED); compensation if needed
erroredCEL did not evaluate to boolStep fails (POLICY_EVALUATION_FAILED); same halt as denial

Denial vs HITL​

Denial and human-in-the-loop review are independent. A policy denial fails the step — it doesn't automatically pause for human review. HITL is configured separately with hitl: true on the step. See HITL review for how that flow works.

You can use both on the same step: the policy gate runs first, and HITL only triggers if the policy passes.

What's next​

Next up: Compensation — declare automatic rollback strategies and safety nets for when things go wrong mid-workflow.