Approval Workflows
When a policy blocks an agent, you can handle it with human approval instead of failing. The on_policy_block parameter lets you define what happens when a block is raised — prompt in the terminal, send a Slack message, call a webhook, or any custom logic.
Quick Start
import waxell_observe as waxell
waxell.init()
@waxell.observe(
agent_name="data-manager",
workflow_name="delete",
enforce_policy=True,
on_policy_block=waxell.prompt_approval, # terminal Y/N prompt
)
async def delete_records(table: str) -> dict:
return {"deleted": 500, "table": table}
When a policy blocks execution, prompt_approval shows a terminal prompt:
!! BLOCKED BY APPROVAL POLICY
Reason: Workflow 'delete' requires approval before execution
Operation: delete
Approvers: dba-team@company.com, compliance@company.com
Timeout: 5 min
Approve this operation? (y/n): y
If approved, the function executes. If denied or timed out, PolicyViolationError propagates.
Built-in Handlers
prompt_approval — Terminal
Interactive terminal prompt with timeout and approver display:
@waxell.observe(
agent_name="my-agent",
enforce_policy=True,
on_policy_block=waxell.prompt_approval,
)
async def sensitive_operation():
...
auto_approve — Testing
Always approves. Use in tests and dry-run scenarios:
@waxell.observe(
agent_name="my-agent",
enforce_policy=True,
on_policy_block=waxell.auto_approve,
)
async def test_approval_flow():
...
auto_deny — Testing
Always denies:
on_policy_block=waxell.auto_deny
Custom Handlers
Write your own handler for any approval channel. The handler receives a PolicyViolationError and returns an ApprovalDecision:
Slack Approval
from waxell_observe.types import ApprovalDecision
from waxell_observe.approval import extract_block_metadata
def slack_approval(error):
meta = extract_block_metadata(error)
# Send Slack message
send_slack_message(
channel="#approvals",
text=f"🔒 *Approval Required*\n"
f"Operation: {meta['action_type']}\n"
f"Reason: {meta['reason']}",
actions=["approve", "deny"],
)
# Wait for reaction
response = wait_for_slack_reaction(timeout=300)
return ApprovalDecision(
approved=(response == "approve"),
approver=response.user,
elapsed_seconds=response.elapsed,
)
@waxell.observe(
agent_name="my-agent",
enforce_policy=True,
on_policy_block=slack_approval,
)
async def guarded_operation():
...
Webhook Approval
import httpx
from waxell_observe.types import ApprovalDecision
from waxell_observe.approval import extract_block_metadata
async def webhook_approval(error):
meta = extract_block_metadata(error)
async with httpx.AsyncClient() as client:
resp = await client.post(
"https://approvals.internal/request",
json={
"operation": meta["action_type"],
"reason": meta["reason"],
"approvers": meta["approvers"],
},
)
result = resp.json()
return ApprovalDecision(
approved=result["decision"] == "approved",
approver=result.get("approved_by", ""),
)
Both sync and async handlers work. Async handlers are awaited automatically.
ApprovalDecision
The return type from all approval handlers:
| Field | Type | Default | Description |
|---|---|---|---|
approved | bool | required | Whether to proceed with execution |
approver | str | "" | Who approved (email, username, system) |
timed_out | bool | False | Whether the approval window expired |
elapsed_seconds | float | None | Time from block to decision |
extract_block_metadata
Helper to extract structured data from a PolicyViolationError:
from waxell_observe.approval import extract_block_metadata
meta = extract_block_metadata(error)
meta["reason"] # "Workflow 'delete' requires approval"
meta["action_type"] # "delete"
meta["approvers"] # ["dba-team@company.com"]
meta["timeout_minutes"] # 5.0 or None
With the Context Manager
from waxell_observe import WaxellContext
with WaxellContext(
agent_name="my-agent",
workflow_name="delete",
enforce_policy=True,
on_policy_block=waxell.prompt_approval,
) as ctx:
result = perform_deletion()
ctx.set_result(result)
What Gets Traced
When an approval handler runs, Waxell automatically records:
approval_requestgovernance event — logged before the handler is invokedhuman_turn:approvalIO span — wraps the handler, captures prompt/response/durationapproval_responsegovernance event — logged after the handler returns
All three appear in the trace timeline without any extra instrumentation code.
Full Example
import asyncio
import waxell_observe as waxell
from waxell_observe.errors import PolicyViolationError
waxell.init()
@waxell.tool(tool_type="database")
def delete_records(table: str, filter_criteria: str = "") -> dict:
"""Simulated deletion — triggers approval policy."""
return {"table": table, "deleted": 500, "status": "completed"}
@waxell.observe(
agent_name="data-manager",
enforce_policy=True,
on_policy_block=waxell.prompt_approval,
)
async def run_agent():
command = waxell.input("> ")
if "delete" in command:
try:
result = delete_records(table="users")
print(f"Done: {result}")
except PolicyViolationError as e:
print(f"Blocked: {e}")
asyncio.run(run_agent())
Next Steps
- Human-in-the-Loop — Capture any interactive input, not just approvals
- Policy & Governance — Pre-execution and mid-execution policy checks
- Policy Categories — All 26 policy categories including Approval