Skip to content
Subako Docs
Esc
navigateopen⌘Jpreview
On this page

Approvals

Put a human in front of a tool call, and settle it from your own UI.

Some tool calls should not happen because a model decided they should — issuing a refund, sending an email, deleting a record. For those, the run pauses and asks.

An approval is a pair of events on the session log, so it works the same whether the person deciding is watching a live stream or opening the session an hour later.

The two events

When a gated call comes up, the run appends approval_requested and waits:

Field What it holds
call_id The call being decided
server Which server the tool belongs to
tool The tool name
arguments What the model wants to call it with
args_hash The identity a re-fire is checked against

arguments is carried on the request event on purpose: whoever decides can see what they are deciding without reading back through the log to find the tool_call it follows.

Settling appends approval_resolved, carrying the decision (allow or deny), an optional deny_message explaining the refusal to the model, and resolved_by.

Building the UI

Watch for the request

A client already tailing the session stream sees approval_requested like any other event. Render it from the event’s own fields.

Show what is actually being asked

Render arguments verbatim. The whole value of an approval gate is that a person sees the real call, not a summary of it.

Settle it

Post the decision to the call_id the request named.

POST /v1/sessions/{session_id}/approvals/{call_id}
{ "decision": "deny", "message": "Refunds over $500 need a manager." }

On allow, the call fires and its tool_result follows. On deny, the model receives the denial — with your message if you set one — and the turn continues from there.

message belongs to deny alone. Sending one alongside {"decision": "allow"} is rejected rather than ignored, so an allow cannot quietly carry steering text the model never sees.

Decide promptly. The broker holding the call reads your decision on its next tick and answers the engine in line; past that hold, the run requeues and re-fires the call instead — which is what args_hash exists to check.

Denial is not failure

A denied call is a normal outcome, not an error. The model gets a tool result saying so and carries on, which is why deny_message is worth writing: “not approved” leaves the model guessing, while “refunds over $500 need a manager” tells it what to do next.

Was this page helpful?