requires_action, and the session
reports what it needs. You do the work wherever your code runs, send the result
back, and the turn resumes from where it paused.
Declaring a function
Give the session a tool the agent can call but the sandbox cannot execute.mcp servers and skill entries; PUT replaces the whole
configuration rather than merging, so send the complete list every time. You can
also pass agent.tools when creating the session.
Noticing
Three ways, in increasing order of how much you have to do:- Webhook — subscribe to
session.action_required. - Event stream — watch for
tool.required. - Polling — a session whose
required_actionsis non-empty.
All three are answerable through this API.
approval and question can also be
answered by a person in the Console, which is often what you want — the point is
that an API-only integration is no longer stuck waiting for one.
Answering
tools:respond scope — and only that one. A token holding
sessions:read and tools:respond can watch this session and answer its
function calls while being unable to send it a message, cancel its turn or
delete it, which is what you want a fleet of tool handlers to hold.
turn_id and call_id are copied from the required action. output must be
a string — serialise it yourself. We cannot know whether your object was meant
as JSON text or as a structure the model should see some other way, and guessing
would be silent either way.
To report a failure, send "success": false with "error" instead of
"output". Tell the agent what went wrong; it can often recover, and it
certainly cannot if you say nothing.
The four outcomes
not_delivered does not end anything, and the row it names is closed. The
sandbox was not holding that call when your answer arrived — usually because it
was restarted underneath the turn. Two sentences on this page used to disagree
about what happens next: this table said “expect it to fail or time out”, while
Deadlines below says, correctly, that nothing times a required
action out. The second one is true. The turn goes back to looking like ordinary
work in progress, and the only thing that will eventually end it is the
workspace’s own two-hour ceiling.
So not_delivered is the one outcome you have to handle rather than log. The
session will read working with an empty required_actions, which is
indistinguishable from healthy — on every surface, including webhooks and the
event stream. Treat it as “this turn lost my answer”: start a fresh turn with
the same information, or fail the job and say why. Do not wait.
already_resolved is the one worth designing for: at-least-once delivery on the
notification side means you will sometimes answer twice, and answering twice is
legitimate rather than a bug to guard against.
Deadlines
By default there is no deadline. Nothing times a required action out. The turn waits for you, the workspace is not paused underneath it, and the session staysrequires_action until you answer.
That is the right default and the wrong one to leave alone in production: a
process that dies mid-answer holds a workspace until its age cap with nothing
anywhere saying so. So you can declare your own, per tool:
expires_at in Unix milliseconds, or null
when it has none. Accepted range is 1 to 7200 seconds — the ceiling is the
workspace’s own lifetime, because a deadline past the point where the session
is reclaimed could never arrive.
Approvals and questions never expire. Those are waiting on a person in the
Console, and timing one out would be us deciding somebody took too long to
think.
Whether or not you set one, the workspace’s own ceiling still applies and
belongs to the session rather than to the action: a workspace is reclaimed two
hours after it starts, and a turn still parked when that arrives ends as
failed. That number lives in limits.md and nowhere else.
So: for a decision that might take minutes, answer whenever you are ready. For
one that might take overnight — a person approving a production change, say —
do not hold the turn. Answer the tool with “queued for review” and start a fresh
round when the decision arrives; see
Approvals in your own product.
When it goes wrong
More symptoms, across the whole API, in
troubleshooting.md; the shape every refusal arrives in is
errors.md.
Compared with webhooks
Because the turn is a single agent run, not a sequence of requests. The agent paused mid-reasoning with its context intact; when your result arrives it continues from exactly there. A design that ended the turn and started another would lose that, and would make every function call a fresh conversation.Next
- input — the four things you can send,
input.tool_resultamong them - tools — declare the functions the agent may call
- approvals in your product — put a person in the loop