Your first policy
A policy carries two things: typed columns for the common cases, and a generalrules
array for everything else. The columns are shorthand that compiles into the same rules, so use
them when they fit.
The typed columns, and the rule name each produces
This mapping matters, because the name you see indecision.rule is not the underlying
rule kind:
Write the same limit in the
rules array instead and it reports under its rule_name, which
defaults to the kind. Both are fine; just do not expect a cap set through
per_transaction_cap_cents to come back as cost_cap.
Rule kinds
That table is the whole registry. There is no way to define a rule kind of your own, and no
kind outside those eight will save.
Every kind takes an optional rule_name, echoed back in the decision, and every kind except
action_allowlist takes an optional action_types array to scope it; an empty array means all
action types. Every rule is assigned a stable id on write, so a decision read months later can
still name the exact rule that produced it.
An unknown kind, or a missing setting, is rejected when the policy is written. Never when an
agent is waiting on an answer.
Two things the rule kinds deliberately do not do, because people ask. rate_limit and
spend_window count across the whole agent and can only be narrowed by action_types: there is
no grouping by counterparty, by resource, or by a metadata key. And nothing reads the clock
beyond a rolling window, so there is no time-of-day or day-of-week condition. Where a fact like
that matters, compute it on your side and send it in metadata, which a condition rule can
read.
An
allow array that is empty is a no-op, not a deny-all. To deny by default, use
mode: "allowlist" instead.pending, requires_approval or completed, and
payment intents in pending, requires_approval or executed. Spend on one
consumes the other’s headroom.
The condition language
condition is the open one: a boolean expression over the action, with an effect.
deny (the default), allow, or require_approval.
Combinators are all_of, any_of and not, nestable to ten levels. An empty all_of or
any_of is rejected at write time.
Fields are action_type, resource, cost_cents, currency, agent_id,
agent_mode, budget_id, and any metadata.* dot path.
A metadata.* rule reads the metadata object on the request, on both
POST /control/v1/agent/actions and POST /control/v1/agent/payment_intents. On the payment endpoint
decision_context is provenance and is never evaluated, so send anything a rule must see as
metadata. A payment is evaluated as action_type payment.create with the counterparty as
its resource, so the same rule covers both endpoints.
Operators are eq, ne, gt, gte, lt, lte, in, not_in, contains,
starts_with, ends_with, exists.
A predicate compares to a literal value, or to another field on the same request with
value_field, which accepts exactly the names the left side accepts. Sending both is rejected
at write time. That is how a refund gets capped at the order it belongs to: see stop an agent
over-refunding.
A missing field never raises. It fails to match. Unknown fields and operators are
rejected at write time.
The ordered comparisons (gt, gte, lt, lte) are numeric. A non-numeric operand makes the
comparison false rather than an error, so a typo in a metadata.* path silently fails to match
rather than blowing up. Use exists when you need to tell “absent” from “false”.
Blocklist and allowlist
A policy isblocklist by default: the action is permitted unless a rule denies it.
Set mode: "allowlist" and it inverts. The action is denied unless one of that policy’s allow
rules matches, reported as the rule not_allowlisted. A deny rule still overrides an allow.
Each allowlist policy is an independent gate. Attach two and both must be satisfied.
Reusable modules
Modules are named rule sets scoped to a workspace. A policy imports them by name and they compile in ahead of its own rules.name, not by id, so GET /control/v1/workspaces/{slug}/policy_modules/vendor-allowlist
reads one back.
Editing a module changes every policy importing it. A policy importing an unknown module will
not save, and a module still imported by a policy will not delete. Modules do not import other
modules, so composition is one level deep and cannot cycle.
Test before you apply
Never guess what a policy does. Preview a draft you have not saved:200 OK
limit_cents, not the per_transaction_cap_cents you wrote. The column name
is the input; detail speaks in the compiled rule’s terms.
Nothing is persisted: no policy, no budget, no intent, no audit event. imports resolve
against the workspace’s real saved modules, so you can preview a draft that builds on modules
already in place. session_budget_cents sets the synthetic budget and defaults to
100000.
Previewing needs only the read capability, deliberately: it is not a mutation.
The same evaluation is available to your agent at POST /control/v1/agent/actions/simulate, which does
record an action.simulated audit event.
Next steps
Holding spend for a person
What
approval_threshold produces, and how it resolves.Spend tokens
A single-use ceiling that narrows a policy for one payment.
Errors
Every rule that can appear in
decision.rule.API reference
The full policy schema.