Skip to main content
How the agent calls your code. This is the one genuinely unusual thing in this API, and the one nobody guesses from the endpoint list. The shape: the agent stops mid-turn, the session goes to 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.
The same call takes 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:
  1. Webhook — subscribe to session.action_required.
  2. Event stream — watch for tool.required.
  3. Polling — a session whose required_actions is non-empty.
Three types appear here: 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

Needs the 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

Named rather than collapsed into ok/not-ok, because they call for different things next: 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 stays requires_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:
Per tool rather than per session, because the answer is a property of the function. A billing lookup that has not replied in thirty seconds is not going to; an approval may legitimately take until morning. One number for the whole session would force the slowest tool’s patience on every other one. Past the deadline the call fails and the turn carries on. It does not fail the turn. The agent has usually done real work before it reached your tool, and throwing that away because your process died is a worse outcome than the one the deadline exists to prevent — so a timed-out function is an ordinary failure, the agent is told not to retry it, and it continues or reports that it could not. This is also what makes the number safe to guess: too short costs you one failed call, not a lost turn. Each pending action reports its own 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_result among them
  • tools — declare the functions the agent may call
  • approvals in your product — put a person in the loop