Appearance
Approval Flows
Some tools should not run without a human nod — PR merges, pod deletions, production SQL writes. Agentcy's approval system pauses the agent loop mid-stream, emits an approval_required event, and blocks on a Postgres record + Tokio oneshot until a decision arrives.
The concept is in Agent Loop — Approvals. This page is the operator and integrator view.
When approvals fire
A tool call requires approval when any of these is true:
- The
ToolSpec.requires_approval = true. - The tool's
effectiswriteordestructiveand the org default requires approval for that effect. - A policy explicitly demands it (Rego rule).
The org defaults:
json
POST /api/v1/settings/security
{
"approval_defaults": {
"read": "allow",
"write": "approve",
"destructive": "approve"
},
"approval_timeout_secs": 300
}The stream-level UX
event: tool_call_start
data: {"tool_call_id":"tc_01","name":"execute_connector_tool","connector":"kubernetes"}
event: approval_required
data: {
"approval_id":"apr_01",
"tool":"kubernetes.delete_pod",
"args":{"cluster":"prod-us","pod":"checkout-6b21"},
"effect":"destructive",
"reason":"destructive tool requires approval (policy: default-destructive)",
"expires_at":"2026-04-24T12:05:00Z"
}
(keepalives while we wait)
event: ping
data: {}Your frontend should render this as a card with Approve / Deny / a reason input, and call:
bash
# Approve
curl -X POST "http://…/chat/conversations/$CONV/approvals/$APR_ID" \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"approved":true}'
# Deny with a note
curl -X POST "http://…/chat/conversations/$CONV/approvals/$APR_ID" \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"approved":false,"note":"we froze prod until Thursday"}'The agent resumes immediately. On deny, the tool call errors with policy_denied and the LLM sees the note; it usually acknowledges and tries a safer path.
Timeout behavior
If no decision arrives before expires_at:
- The pending record flips to
expired. - The tool call errors with
approval_timeout. - The loop emits
message_endwithfinish_reason: "approval_timeout". - The UI can re-ask (a new conversation turn).
Default timeout is 300 seconds. Override per-conversation:
bash
curl -X PATCH "http://…/chat/conversations/$CONV" \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"approval_timeout_secs":900}'Escalation
For long-running tasks and channels (WhatsApp, Slack), a 5-minute timeout is often too short. Agentcy supports escalation:
json
{
"escalation": [
{ "after_secs": 120, "notify": "slack:#ops" },
{ "after_secs": 300, "notify": "email:oncall@…" },
{ "after_secs": 900, "then": "deny" }
]
}Set per-tool (in its ToolSpec) or per-conversation (via PATCH). The notify side uses the configured channel adapters.
Batch approvals
An agent might queue several tool calls behind an approval wall. Approve all at once:
bash
curl -X POST "http://…/chat/conversations/$CONV/approvals/batch" \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"approval_ids":["apr_01","apr_02","apr_03"],"approved":true}'Who can approve
By default, any user in the org can approve any pending call in their conversations. Tighten with a Rego policy:
rego
deny[msg] {
input.action == "chat.approval.decide"
input.resource.tool_effect == "destructive"
not input.subject.role in {"admin","owner"}
msg := "only admins can approve destructive tools"
}Programmatic approvers
If you want a bot/agent to auto-approve certain classes of tool calls (e.g. reads from non-prod clusters), issue an API key with a scoped role and run a small subscriber on the activity feed:
python
import requests, os
TOKEN = os.environ["AGENTCY_API_KEY"]
BASE = "https://your-agentcy/api/v1"
with requests.get(f"{BASE}/activity/stream?types=approval.requested",
headers={"authorization":f"Bearer {TOKEN}"}, stream=True) as r:
for line in r.iter_lines():
if not line.startswith(b"data:"): continue
ev = json.loads(line[6:])
if ev["resource"]["tool"] == "kubernetes.list_pods":
requests.post(
f"{BASE}/chat/conversations/{ev['conversation_id']}/approvals/{ev['approval_id']}",
headers={"authorization":f"Bearer {TOKEN}"},
json={"approved": True},
)Listing pending approvals
bash
curl "http://…/approvals?status=pending&limit=50" -H "authorization: Bearer $TOKEN" | jqThe UI's top-bar bell uses this.
Gotchas
- Don't lose the approval_id. If your client drops the SSE stream between
approval_requiredand the decision, reconnect with?after=to re-receive it. Treatapproval_idas a durable handle. - Approvals do not cache. Approving
kubernetes.delete_pod{cluster=prod,pod=checkout-6b21}does not auto-approve the same tool next time. Use policies for "always allow X". - Denied calls still cost LLM tokens. The model has to re-plan. Write tight policies to prevent repeated dead-end tool calls.
Next
- Concept: Policies — codify approval rules.
- How-To: Rego Policies — templates.
- How-To: Audit Log — every approval is logged.