Before you start
A token and a model, as in the quickstart. You do not need a public HTTPS endpoint — this page uses polling, which runs on a laptop behind a firewall. Webhooks are an optimisation, and they come last.The whole thing
input() with whatever your product already uses to ask a person.
Nothing else about the loop changes.
Three choices worth copying
The tool executes nothing. It records a decision; the agent carries the action out itself once approved. That keeps the approval free of side effects, which is what makes it safe to deliver twice.blast_radius is required. An approval request that does not say what is at
stake cannot be judged, and a model will happily omit it if you let it.
“If you are refused, propose an alternative” is in the instructions. What an
agent does after a refusal is a branch you design, not a default you inherit.
The type you did not expect
required_actions holds three types, and only one of them is yours:
All three are answerable through this API. They were not: for a while only
function_call was, and this page said so and called it uncomfortable. The
advice below about telling the agent not to ask is still useful when nothing in
your product can answer a question — but it is now a choice rather than the
only way out.
An earlier draft of this page used an on-call scenario. Run against a real
session, the agent did not call request_approval — it asked a clarifying
question, which arrives in the same list as
{"type": "question", "name": "…the sandbox is empty, how should I proceed?"}.
Adding the log file it said it was missing did not fix it; it asked again.
So a poller that assumes every entry is a function call answers a question
wrongly and then waits forever for a turn that is still blocked. Two things
follow, and the script does both:
Branch on type. Each has its own event, and sending the wrong one is a
400 that names the right one:
approved must be true or false — it is not defaulted either way, because
denying something you meant to allow is a mistake and the other direction is
worse. answer reaches the model as written.
If nothing was waiting on that call_id — already answered, or the turn moved
on — you get 202 with "outcome": "no_longer_pending" rather than a refusal,
because that is what a retry after a dropped connection looks like.
Tell the agent not to ask. One line in instructions —
question, the agent must not be allowed to raise one.
Refusing is not failing
success describes your tool, not the decision. A refusal is your tool
working perfectly and returning “no”.
success: False means your approval system itself broke — and the agent will
try to recover from a fault that did not happen, usually by retrying the call
you meant to deny.
Include who approved and when. The transcript then holds the audit trail
instead of it living only in your logs.
Answering twice
A dropped connection duringPOST /events leaves you not knowing whether the
answer landed. Send it again: the second one comes back
accepted,
already_resolved and not_delivered — the last meaning the call timed out
while you were deciding. All three are normal; see input.md.
Swapping polling for webhooks
Once you have a public HTTPS endpoint, stop polling:secret, once. Store it; it is not shown again, and
verifying the signature before trusting a body is not optional —
webhooks.md has the formula.
Read the session when the webhook arrives; do not act on its payload.
Deliveries are at-least-once and may arrive out of order. The notification tells
you to look; GET /v1/sessions/{session_id} is what is true. The loop above is
already written that way, which is why moving to webhooks changes only what
wakes it.
When it goes wrong
You have it working when
- The agent stops before acting, and
required_actionsnames your tool with all three fields filled in - A refusal makes it propose something else rather than proceed
- Sending the same
call_idtwice reportsalready_resolved - A
questionin the list does not break your loop
Next
- required-actions.md — the full object, deadlines, expiry
- input.md — the other three things you can send a session
- Stream the agent’s progress into your UI — showing the investigation, not just the question