Approvals¶
Reviews and decides on PendingApproval rows: Orchestrator tool calls
deferred behind requires_approval
instead of running immediately. See that page for how a tool ends up
here in the first place; this page covers listing pending requests and
deciding them.
Gated behind a deployment setting, Admin session token required
Same two gates as Orchestrator tools: every
endpoint here only exists when your deployment has Orchestrators
turned on, and every endpoint here requires an admin session token
with Admin or Owner, not just any logged-in admin. Deciding a
pending request is exactly as consequential as turning
requires_approval on in the first place, so it's held to the same
tier.
Listing pending approvals¶
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
status_filter |
"pending" | "approved" | "rejected" |
No | none (unfiltered) | Restrict results to one status. |
orchestrator_id |
integer | No | none (unfiltered) | Restrict results to one Orchestrator's own requests. |
Results are always ordered newest first (by id descending). Both
filters can be combined (as an AND).
[
{
"id": 12,
"orchestrator_id": 3,
"tool_name": "send_email",
"arguments": {
"to": "customer@example.com",
"subject": "Your refund",
"body_html": "<p>Your refund has been approved.</p>"
},
"status": "pending",
"result": null,
"note": null,
"decided_at": null,
"decided_by_admin_id": null,
"created_at": "2026-01-15T10:12:00Z"
}
]
Fields¶
| Field | Type | Description |
|---|---|---|
id |
integer | The request's id, used in the approve and reject paths below. |
orchestrator_id |
integer | Which Orchestrator's run created this request. |
tool_name |
string | Which tool was called, e.g. send_email. |
arguments |
object | The exact keyword arguments the model supplied to the tool call, unmodified. Approving replays the real tool with exactly these arguments. |
status |
string | pending, approved, or rejected. |
result |
string or null | The real tool's own return string, populated only once approved and actually executed. Stays null for a rejected or still-pending request. |
note |
string or null | An optional note attached when rejecting. Never set by approving. |
decided_at |
datetime or null | When an Admin approved or rejected this request. null while still pending. |
decided_by_admin_id |
integer or null | Which admin decided it. null while still pending, and stays null forever if that admin account is later deleted (see Audit log's same admin_id nullability). |
created_at |
datetime | When the tool call was queued. |
Approving a request¶
Rebuilds the real tool the same way the agent-tree builder would have,
and actually invokes it with the exact arguments above. This is not a
status change alone, it's the deferred action genuinely happening, for
real, for the first time.
Returns the updated row, with status: "approved", result populated
with whatever the real tool returned, and decided_at/
decided_by_admin_id set. A nonexistent approval_id gets a 404; a
request that isn't pending anymore (already approved or rejected by
someone else) gets a 409, never a silent double-execution.
Rejecting a request¶
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
note |
string or null | No | null |
An optional reason, shown on the row. |
Never invokes the real tool. result stays null forever for a rejected
request.
const response = await fetch(
"https://api.your-domain.com/api/v1/pending-approvals/12/reject",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({
note: "Refund amount looks wrong, following up with the customer first.",
}),
},
);
const approval = await response.json();
Same 404/409 behavior as approving, for the same reasons.
Being told a request is waiting¶
A request sits until somebody looks at it, so a deployment can be told the
moment one is queued instead of discovering it later. The
approval.requested webhook fires either way,
but that only helps when something is listening for it. These channels reach
a person.
GET /api/v1/approval-notifications returns the current settings, and
PUT updates them. Both take an admin session token with Admin or
Owner, the same tier as deciding a request. Every channel is off until
turned on, and any combination can run at once.
| Field | Type | Notes |
|---|---|---|
email_enabled |
boolean | Off by default. |
email_address |
string | Falls back to the deployment's REQUEST_APPROVAL_EMAIL. |
telegram_enabled |
boolean | Off by default. |
telegram_chat_id |
string | Falls back to the deployment's TELEGRAM_CHAT_ID. |
telegram_bot_token |
string | Write-only. Falls back to TELEGRAM_BOT_TOKEN. Send "" to clear it. |
whatsapp_enabled |
boolean | Off by default. |
whatsapp_to_number |
string | The number to message. |
whatsapp_from_number |
string | The E.164 WhatsApp-enabled number to send from. |
whatsapp_content_sid |
string | An approved Content Template. Required; see the warning below. |
call_enabled |
boolean | Off by default. |
call_to_number |
string | The number to call. |
call_from_number |
string | The E.164 Twilio number to call from. |
A field left out of a PUT is unchanged, so one channel can be edited
without resending the rest. telegram_bot_token is never returned; the
response carries telegram_bot_token_set instead, so a client can show
whether one is stored without being able to read it.
WhatsApp needs an approved template, not just a number
A notification is business-initiated, which WhatsApp only permits
through a Content Template Meta has approved. A whatsapp_to_number
without a whatsapp_content_sid sends nothing. The template is called
with the request id as {{1}} and the tool name as {{2}}. The other
three channels need no such approval.
The call channel speaks a short announcement and hangs up. It is not a
conversation: no agent, no transcript, and no Call record, because there
is nobody on the other end to talk to.
A channel that is on but incompletely configured logs and is skipped rather than failing the approval. Queuing a request must never depend on a notification succeeding.
From the command line, scripts/set_approval_notifications.py edits the
same settings:
python -m scripts.set_approval_notifications --email ops@example.com
python -m scripts.set_approval_notifications --show
In the dashboard, the Approvals page's Notify me on request button opens the same settings.
Next¶
- Orchestrator tools: how a
tool gets set to
requires_approvalin the first place. - Audit log: every queue and every decision made here is also recorded there.
- Webhook subscriptions: the
approval.requestedevent, which fires whatever the notification channels above are set to.