Policy authoring
Match rules, condition operators, verdicts, approvers, timeouts, and escalation — the full policy schema.
A policy decides, for each approval request, whether a human is
needed and who reviews it. A request references a policy by its policyKey; the policy with
the highest version for that key and org is evaluated.
Shape
{
"match": { "action": "wire.transfer", "agent": "finance-bot" },
"rules": [
{
"when": [{ "field": "payload.amount", "op": "gt", "value": 1000 }],
"then": "require_approval"
},
{ "when": [], "then": "allow" }
],
"approvers": {
"channel": "slack",
"users": ["U0123ABC"],
"teams": [],
"escalation": [{ "channel": "email", "targets": ["oncall@acme.dev"], "timeoutSeconds": 1800 }],
"timeoutSeconds": 900,
"onTimeout": "escalate"
}
}Match
The policy is only applied if match matches the request. Both fields are optional; an
omitted field matches anything.
| Field | Matches when |
|---|---|
action | the request's action equals this value |
agent | the request's agent equals this value |
If match does not match, the engine fails closed to
require_approval.
Rules
rules is an ordered list. The first rule whose conditions all pass wins, and its
then verdict is returned. A rule with an empty when array always passes — use it as a
catch-all final rule.
then is one of allow, deny, or require_approval. If no rule matches, the fallthrough
verdict is require_approval.
Conditions
Each condition is { field, op, value }. field is a dot-path evaluated against the
request root, so payload.amount, context.tier, action, and agent are all reachable.
A missing field never passes.
op | Passes when |
|---|---|
eq | field deep-equals value |
neq | field does not deep-equal value |
gt | field and value are both finite numbers and field > value |
lt | field and value are both finite numbers and field < value |
contains | field is a string containing value, or an array containing value |
regex | field is a string matching the value pattern |
gt / lt never coerce. A string "5000" is not greater than 1000 — it fails closed. Send
numbers as JSON numbers.
Approvers
When the verdict is require_approval, the approvers block decides who is notified and how
long they have.
| Field | Meaning |
|---|---|
channel | slack, email, or dashboard — where the request is delivered |
users | channel-specific reviewer ids (e.g. Slack user ids) |
teams | channel-specific team ids |
timeoutSeconds | how long the request stays pending before it times out |
onTimeout | deny (default) or escalate when the timeout elapses |
escalation | ordered next steps, each { channel, targets, timeoutSeconds } |
If onTimeout is escalate, the request moves to the next escalation step instead of
being denied. When the steps run out, it is denied — Seal never leaves a request pending
forever.
Managing policies
Policies are stored per org and versioned. They are authored in the dashboard; for a local
dev instance, pnpm db:seed installs a sample policy under the key payments.high_value.