Skip to main content

Approval Policy

The approval policy category puts a human in the loop before sensitive agent operations. Use it to require approval before a workflow runs, before specific tools or action types execute, or when the in-flight cost crosses a threshold. Also supports session-start gates (E12) for chat-style agents and auto-approval below a risk level.

Rules

RuleTypeDefaultDescription
require_approval_forstring[]["deploy", "delete", "payment"]Workflow names, action types, tool names, OR the special "session-start" token
cost_thresholdnumber100.00Trigger approval when context.cost_used exceeds this amount
approversstring[][]Emails or group IDs surfaced in violation metadata (used by your approval workflow)
timeout_minutesinteger30Deadline -- surfaced in metadata for your approval workflow
action_on_timeoutstring"block"block raises PolicyViolationError, warn lets the run continue
auto_approve_below_riskstring"low"Auto-approve actions with risk_level at or below this (none, low, medium, high, critical)

How It Works

The approval handler runs at before_workflow, mid_execution, and after_workflow.

PhaseWhat It ChecksActions
before_workflow"session-start" in require_approval_for, or workflow_type/workflow_name is in the listBLOCK or WARN per action_on_timeout
mid_executionEach pending_action against require_approval_for (with auto_approve_below_risk skip), cost threshold, tools_used against the listBLOCK or WARN
after_workflowAudit: which restricted actions actually executed and whether cost exceeded thresholdWARN (audit-only)

Context Attributes Read

AttributePhasePurpose
context.workflow_type / context.workflow_namebefore_workflowMatch against require_approval_for
context.pending_actionsmid_execution, after_workflowList of {type, risk_level} dicts (or strings) the agent wants to run
context.cost_usedmid_execution, after_workflowCompare to cost_threshold
context.tools_usedmid_execution, after_workflowTool names checked against require_approval_for

When a gate fires, the result metadata includes requires_approval: True, approvers, timeout_minutes, and action_on_timeout. The handler does not block on a human response -- your inbox / Cowork session-bootstrap code reads that metadata and routes to the approval flow (see services/session_approval.py for the session-start path).

Example Policy

{
"name": "Production Approval Gates",
"category": "approval",
"rules": {
"require_approval_for": ["deploy", "delete", "send_email", "make_purchase"],
"cost_threshold": 25.00,
"approvers": ["ops-team@acme.com", "finance@acme.com"],
"timeout_minutes": 60,
"action_on_timeout": "block",
"auto_approve_below_risk": "low"
},
"scope": {"agents": ["ops-agent"]},
"enabled": true
}

Session-Start Approval (E12)

{
"name": "Session Bootstrap Approval",
"category": "approval",
"rules": {
"require_approval_for": ["session-start"],
"approvers": ["security@acme.com"],
"timeout_minutes": 15,
"action_on_timeout": "block"
}
}

SDK Integration

import waxell_observe as waxell
waxell.init()

@waxell.observe(agent_name="ops-agent", enforce_policy=True)
async def run_deploy(target: str) -> str:
# before_workflow checks workflow_name against require_approval_for
# mid_execution checks pending_actions / tools_used / cost_used
return await deploy(target)

Observability

FieldExample
Categoryapproval
Actionblock
Reason"Action 'payment' requires approval"
Metadata{"requires_approval": true, "approvers": ["finance@acme.com"], "action_type": "payment", "timeout_minutes": 60, "action_on_timeout": "block"}
FieldExample (cost trigger)
Reason"Cost ($28.45) exceeds approval threshold ($25.00)"

Common Gotchas

  1. The handler does not perform approval -- it raises the signal. A BLOCK result raises PolicyViolationError. Your calling code (Cowork session bootstrap, inbox, or a custom router) inspects metadata["requires_approval"] and runs the actual approval flow.
  2. auto_approve_below_risk requires risk_level on each pending action. If your agent emits pending_actions as plain strings (no dict), the risk-level skip is never taken and every match in require_approval_for fires the gate.
  3. session-start is a magic string. It does not match against workflow_name -- it triggers if the literal "session-start" appears in require_approval_for. The metadata includes approval_kind: "session-start" so routing code can tell session gates apart from workflow gates.
  4. cost_threshold checks context.cost_used, not the cost of the next action. The check fires after the cost has already been incurred. To gate before spending money, pair this with the Budget Policy per_workflow_cost_limit.
  5. require_approval_for is a flat list shared across workflow names, action types, and tool names. A tool called delete and a workflow named delete both match the same string. Use distinct names if you need different behavior.
  6. after_workflow always returns WARN if any restricted action ran, not BLOCK. It is an audit signal -- the run has already happened. Use mid_execution BLOCK to actually halt.

Next Steps