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 (read at gate time; the step row stores the policy: path ref, not this field)
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')"

The engine loads the file when the gate runs. warden deploy also validates that each referenced policy file exists and that its CEL compiles. A policy that goes missing after deploy surfaces as errored at runtime. Keep policy files on disk where the engine container can read them — 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 saga step

Reference a policy by path on any step that should be gated:

steps:
- id: post-comment
kind: commit
worker: github-demo-worker
policy: github-issue-comment.yaml
# with, tools, when, hitl, … omitted — see saga manifests

Field placement and with bindings are saga-manifest concerns — see 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. truepassed; falsedenied. 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.