Skip to main content

Approve risky actions before they run BETA

escalate_on_deny does not raise an approval request

The deny fires. A rule tagged escalate_on_deny: true denies exactly as written -- the matched rule's own policy_id, effect: "deny", reason_code: "RULE_MATCH". You are protected.

The escalation does not. The tag is accepted by the policy schema and carried into the policy bundle, and no enforcer acts on it: no approval request is raised, no approver is notified, and decision.requires_approval stays false. Code that branches on decision.requires_approval in order to act on this tag therefore never runs. (A different mechanism, LLM function policies' require_approval, does set that field -- escalate_on_deny is not wired to it.) Tracking: #2391.

To request approval today, call it explicitly. client.request_approval(decision) posts a real approval request. Nothing about the tag calls it for you. See Approval callback. Per #2363 the approver-facing /approvals pages are gated in production, so requests are resolved through the API.

This applies to the published Python SDK (controlzero 1.13.14) and the published Node SDK (@controlzero/sdk 1.13.6) alike.

Surfaces used: Approval workflow (dashboard + notification channel) Modes supported: Hosted (SaaS) Tiers: Teams Status: BETA. The request path is shipped on every deployment, including the hosted (SaaS) plan: policy raises the approval request and the agent pauses. Approvals are off by default -- an administrator turns them on per scope (org, project, or API key) under Settings -> Approvals. Enabling needs a separate approver, so it starts on the Teams tier.

The approver's inbox is not reachable yet

The half of this flow that pauses the action works today. The half that lets a human release it does not: the approvals inbox and request detail routes redirect to the dashboard in every shipped deployment, and the notification deep link points at that same path. No tier -- Teams included -- can approve a request from the UI right now.

The consequence is safe but blunt. A paused request runs to its deadline, the SDK raises HITLTimeoutError with a synthesized deny, and the action does not run. Turning approvals on today gives you a hard stop with an audit trail on approval_required actions. It does not yet give you approve-and-proceed. See Set up approvals for the end-to-end guide.

What you'll do

When an AI agent attempts an action your policy marks as approval_required (production deploy, large refund, external email send), the request pauses and an approver gets a Slack or email notification with the full context. Once the inbox ships, the approver clicks Approve or Deny and the agent either proceeds or gets a denial with reason.

Why this is the right path for you

  • If you want AI agents to handle high-stakes work but refuse to let them fire-and-forget, this is the pattern.
  • If your approver pool is small and predictable, the approval flow beats per-request policy edits.
  • If you want to outright deny, use an ordinary DLP rule or policy. Approvals are for "sometimes yes, sometimes no" decisions.

When NOT to use this approach

caution

If the answer is always the same ("never allow production writes from an agent"), do not wait for an approval. Write a deny rule today.

The experience

Agent -> Control Zero: invoke tool "send_refund" args {amount: $9500, customer: ...}
Control Zero: policy match "high_value_refund" -> deny
SDK: your code calls client.request_approval(decision) -> POST approval request
Control Zero -> Slack / email / in-app bell: "Agent 'billing-bot' wants to refund $9500 to cust_123.
[ Approve ] [ Deny ] [ Open context ]"
Approver clicks Approve once / for 24h / 7d / 30d / forever.
SDK: PendingApproval.wait() resolves -> agent proceeds with the call (allow path).

The pause-and-wait primitive ships in the SDKs today: the agent calls request_approval() on a deny it decides is reviewable, then blocks on PendingApproval.wait() until an approver decides. Policy actions, notification channels, and the audit trail all back this flow. The step the agent's own code must take is the request_approval() call -- the policy tag does not make it happen.

How to set it up

  1. Tag the rule. Mark the rule you intend to review with escalate_on_deny: true. Today this records intent for whoever reads the policy; step 3 is what turns the deny into a request.
  2. Turn approvals on. An admin enables approvals for the scope (org, project, or API key) under Settings -> Approvals. They are off by default.
  3. Wire the SDK. On a deny your code treats as reviewable, call request_approval() and wait() (Python) / requestApproval() and wait() (Node). This is the step that raises the request. See the approval callback guide.
  4. Approve from anywhere. The approver acts from the in-app bell, an email magic-link, or a connected alert channel.

Every approval is auditable: who approved, when, why, and what grant was created. See Set up approvals for the full walkthrough and Approval workflow for the decision-kind and scope model.

Prefer an always-on deny?

If the answer is always "no", do not use approvals -- write a deny rule. If you want a lightweight notify-on-deny review loop without enabling approvals, deny with a reason and route it to a channel:

- name: high_value_refund
match:
tool: send_refund
argument_gt: { amount: 1000 }
action: deny
reason: 'Refunds over $1,000 require a human approver. Reply in #approvals to authorize.'

Pair it with alert channels so the denial reason and tool arguments land in your review channel.

Common follow-ups

Reference