Skip to main content
A session is one directly-controlled run of an agent. You create it, send input, stream events, interrupt it, and cancel it. Sessions are durable — they can run for a long time and sit idle at storage cost only.

The session object

string
Unique id (e.g. ses_01H...).
string
The agent this session runs.
integer
The pinned agent version.
string | null
The model the latest send picked, which the next turn runs on. null until a send picks one: the session then runs on its pinned agent version’s model.
string
build or plan — see Plan mode. build until the create or a send picks plan.
string | null
The deployment that created this session, if any.
string | null
The persona this session acts as, or null when the create call named none.
string | null
The computer this session provisioned for its filesystem and shell tools, or null when its toolset needs none. It is a real computer — it lists under GET /v1/computers as session {id} and takes exec, snapshot and fs/* — but it belongs to the session: DELETE on it is refused while the session can still run, and it is destroyed when the session ends.
string
Lifecycle state — see below.
string | null
Why the last turn yielded control — see below. Read it whenever the session is not running; do not treat idle as success.
string
Effective completion window for this session.
integer
Per-session cap, in micro-USD. Always set: it falls back to the agent’s budget.max_task_micro_usd.
integer
Cumulative spend on this session so far, in micro-USD.
object
Cumulative token counts across five billed tiers.
object[]
What the session is parked on. One array, two kinds of row, distinguished by kind — which defaults to "tool", so a row without it is a held tool call.A kind: "tool" row is resolved with tool confirmations and accompanies stop_reason: "awaiting_approval". A kind: "question" row is resolved with answers and accompanies awaiting_answer. A session parked on both reports awaiting_approval and carries both.
object | null
JSON Schema the final result must satisfy, inherited from the agent unless overridden at create.
object | null
The typed result, populated when the session goes idle satisfying output_schema. See Structured outputs.
integer
Highest event seq emitted so far. See Events.
object
Arbitrary key/value pairs.
string | null
When the session first started running.
string | null
When the session reached a terminal state (completed, failed, cancelled).
string
Creation timestamp.

Lifecycle

A session moves through these status values: Whenever a turn yields control, stop_reason is set to one of:
idle is resumable and completed is terminal — both are non-running. Always branch on stop_reason rather than treating idle as success. Anything worth keeping is written to Files; scratch work stays in the sandbox.

Create a session

POST /v1/sessions → 201 Created. Creates and starts the session. Inherits the agent’s configuration; you may override the window, set a per-session budget, and provide an initial message.
string
required
The agent to run.
integer
Pin a specific version. Defaults to the agent’s current_version.
string
required
Initial input. Starts the first turn immediately.
string
Override the completion window (immediate | priority | loose).
integer
Per-session cap in micro-USD. Work pauses at the cap.
string
Attribute this session to a deployment, so its spend shows under that schedule in the ledger. Defaults to null.
string
The persona this session acts as — an idn_ id or the persona’s name. The agent must hold a grant to it. Defaults to null.
object
JSON Schema the final result must satisfy. Overrides the agent default. See Structured outputs.
boolean
When true, a session that cannot produce a conforming object goes idle with stop_reason: "error". Overrides the agent default; requires an output_schema on the session or the agent.
object
Arbitrary metadata.
string
plan or build. plan starts the session in plan mode, so it plans from its first message. The same rules as a send’s mode: refused on a harness that cannot hold a call for approval, and only a person sets it. Defaults to build.

Retrieve a session

GET /v1/sessions/{id} → 200 OK with the current status, stop_reason, and usage.
Response

List sessions

GET /v1/sessions → 200 OK, cursor-paginated. Filter with ?agent_id=, ?deployment_id=, and ?status=. See Pagination. ?handoff_from=ses_… lists the top-level sessions that root handed off — the room’s member work — newest first. A thread the root delegated to is not a handoff; those are GET /v1/sessions/{id}/threads. ?handoff_key=room:ses_… lists the top-level sessions handed off under that exact key — the room’s seated members, whoever seated them — newest first.

Hand off to other agents

POST /v1/sessions/{id}/handoffs → 200 OK. Scope sessions:write. The session {id} — top-level and not yet ended — hands message to one or more agents of your organization, each started as a top-level session of its own marked metadata.handoff: { from_session, from_agent, key, depth }. Who may be reached is the sender’s agent’s handoffs policy, exactly as it governs the model’s own send_to_agent; the receiver’s budget is capped at its agent’s per-task cap.
string[]
required
One to six targets, each an agt_ id or an agent name. Duplicates collapse to one.
string
required
The task. The receiver sees it prefixed with the sending agent and session, so it can tell a handoff from a person.
string
De-duplicates per target: a target still working under this key answers already_running with that session instead of starting another. Under a room’s key (room:<sender id>) the words are delivered into that session for you, and it takes no send of yours — it answers into the room; under any other key, send follow-ups to it yourself.
integer
Per-receiver cap in micro-USD; clamped to the target agent’s max_task_micro_usd.
A refused target is a row, not an error: one target the policy does not allow does not stop the others.
Response
Only the sender’s own faults answer with the error envelope: a sender that is a thread or a member’s session in a room (403 forbidden), has ended (409 session_terminal), or is not yours (404 not_found). A chain of handoffs is at most four deep.

Send input

POST /v1/sessions/{id}/messages → 202 Accepted. Sends input to a session and produces events. The reply is the acceptance, not a session snapshot: the turn has not run yet.
string
required
The message text to send.
boolean
When true, steer a running session in one call: the current turn stops at its next commit boundary and the new message is applied. This is the common case. Defaults to false.
boolean
When true, a send to a running session is accepted and held: the current turn finishes on its own and the message becomes the input of the turn that follows. On a session that is not running it is an ordinary send. Cannot be combined with interrupt. Defaults to false.
string
A model id from GET /v1/models. The session runs on it from this message’s turn on, and later turns keep it until another send picks again. The send is quoted on it, so an unknown id is 400 validation_failed and a model that does not serve the session’s completion window is 400 window_unavailable. The agent itself is not changed — update the agent too if new sessions should start on it.
string
plan or build. plan runs the session read-only from this message’s turn on: it can research, ask and write a plan, and builds only after a person approves it. build leaves plan mode. A session whose harness cannot hold a call for approval refuses plan with 400 validation_failed naming mode. Only a person sets it: a send an agent makes through the control-plane tools is 403 forbidden naming mode. See Plan mode.
string
Accepted and ignored. De-duplicate a retried send with the Idempotency-Key header — see Idempotency; this body field does nothing.
Without interrupt or queue, sending to a running session returns 409 with code session_running — interrupt first, set interrupt: true to steer it, or queue: true to wait for it. A member’s session in a room — one handed off under the key room:<id> — answers into that room, so a send to it (and an interrupt that carries a message) returns 403 forbidden. Speak in the room instead; stopping it, approving its calls and answering its questions work as on any session.
Response
accepted_seq is the session’s event high-water mark at the moment the send was accepted: every event this turn produces has seq > accepted_seq. That is what to wait on — poll GET /v1/sessions/{id}/events?after_seq=143, or wait for the session’s last_seq to pass it. Do not wait on status alone: the send returns before the first event is committed, so a status read taken immediately after can still describe the previous turn. A channel’s session runs nothing. A top-level session whose metadata.room is its own agent’s id is a channel’s transcript. A send to it — and the first message when it is created — is recorded on its log as the person’s message (message.completed with role: "user", under the Idempotency-Key), and no turn follows. Nothing is quoted, interrupt and queue change nothing, and status stays as it was. To ask a member, hand off from it.

Interrupt a session

POST /v1/sessions/{id}/interrupt → 200 OK. Stops the current turn at its next commit boundary, preserving history and the sandbox, and returns the session idle with stop_reason: "interrupted". An optional message is applied as the next input. Unlike cancel, the session stays resumable.
string
Optional input to apply after interrupting.
Response

Respond to a tool confirmation

When a tool with permission: "ask" runs, the session goes idle with stop_reason: "awaiting_approval" and the blocking calls appear in pending_actions. Resolve each with: POST /v1/sessions/{id}/tool_confirmations → 202 Accepted, carrying the full session object with the resolved call removed from pending_actions. Like a send, the reply is the acceptance: the tool has not run yet.
string
required
The id from pending_actions[].tool_call_id (also carried on the tool.confirm event).
string
required
allow to run the tool, or deny to reject it.
string
On deny, fed back to the model as the tool result.
string
default:"call"
How far the allow reaches. call — the default, and what an omitted scope means — approves this one call. session grants the call’s tool for the rest of this session: every later call to that tool name runs without a confirmation. allow only; session with deny is rejected with validation_failed (400) naming scope.
object
A template install’s answers as the person edited them, keyed by question key (a string, or a list for a multiple choice), at most four — a template asks at most four questions. allow only, and only for a call that applies a template: the install runs with these answers and every other argument as proposed. Answers the template’s questions refuse, a deny, scope: "session", or any other tool are rejected with validation_failed (400) naming answers or answers.<key>.
An allow of a parked propose_plan builds the plan at the version the call names. If the plan has been written since — by the agent, or by you through PUT /plan — it is refused with version_conflict (409) naming tool_call_id, and the call stays parked: read the plan, then deny with a reason so the agent proposes the version that is there now. A session grant is deliberately narrow. It covers one tool name, in one session, and it is never implied — you ask for it per grant. It lives in that session’s transcript, so it disappears when the session ends, is invisible to every other session, and is never written onto the agent. It does not apply to a parked question, and an explicit deny for a particular call still stands even while its tool is granted. When the parked call is the platform’s tool invoker (tools_execute, which runs any catalogue tool named in its tool argument), the grant is for that target tool: later tools_execute calls with the same tool run without a confirmation, and a different tool through the same invoker is confirmed as usual. The target is read from the parked call — the body carries nothing extra — and a grant for the invoker with no target grants nothing.
Response

Answer a question

An agent that needs something from a person parks the turn: the session goes idle with stop_reason: "awaiting_answer" and the question appears in pending_actions as a row with kind: "question", carrying a prompt and one to three fields. Answer it with: POST /v1/sessions/{id}/answers → 202 Accepted, carrying the full session object with the answered question removed from pending_actions. Scope sessions:write.
string
required
The id from pending_actions[].tool_call_id on the row you are answering.
object
required
One entry per field, keyed by fields[].key. A string, or an array of strings for a choice field with multiple: true. Every field must be answered, except one the question marked optional: true — say “no answer” by leaving that key out of the object. A key that is present is validated either way, so "" and [] are validation_failed, not a way to skip.
This is a separate route from tool_confirmations, not a variant of it. A held tool call is answered with a verb and no payload; a question is answered with a payload and no verb. Posting an answer for a kind: "tool" row is rejected with validation_failed (400) naming tool_call_id, and points you at the other route. A choice field accepts anything the person types unless it sets other: false, in which case only its listed options are accepted. The values are read back by the model as words, so send the option text and not an index.
Response

Read the plan

GET /v1/sessions/{id}/plan → 200 OK. Scope sessions:read. The session’s living plan: { "object": "plan", "markdown": "...", "version": 3, "by": "owner" }. version is 0 and markdown empty until one is written; by is agent, owner, or null at version 0.

Edit the plan

PUT /v1/sessions/{id}/plan → 200 OK, carrying the plan. Scope sessions:write. The owner’s edit, a compare-and-set.
string
required
The whole plan, at most 20,000 characters. - [ ] and - [x] lines are its to-dos.
integer
required
The version you edited from.
The write wakes nothing: the agent reads the edit at its next turn.
Response
Errors — version_conflict (409) naming base_version when the plan moved on since you read it; read it again and edit from there. forbidden (403) on a member’s session in a room; session_terminal (409) on an ended session.

Raise the budget

PATCH /v1/sessions/{id}/budget → 200 OK. Scope sessions:write. A session that hit its cap stops with stop_reason: "budget_paused". Raise the cap and it becomes resumable.
integer
required
The new per-session cap, in micro-USD.
Raise only. A cap at or below what the session has already consumed is rejected with validation_failed (400) — lowering it would be retroactive, charging a limit against spend that already happened.
Returns the full session object with the new budget_micro_usd. Errors — validation_failed (400) for a cap that is not a raise; not_found (404); insufficient_credits (402) when the organization cannot cover the new cap.

Cancel a session

POST /v1/sessions/{id}/cancel → 200 OK. Terminally stops the session and moves it to cancelled. Any in-flight turn is stopped. Use interrupt instead if you want to keep the session resumable.
Response

Download a workspace

GET /v1/sessions/{id}/workspaces/{phase} → 200 OK with the archived working directory of a session’s sandbox, as it was at one boundary of its work: end (the session finished, or its idle sandbox was reclaimed), or card_start / card_end (it moved a board card to doing / done). start has none — a fresh sandbox’s disk is its base image. The body is a tar.gz; its sha-256 is in the x-workspace-sha256 header, and an unchanged workspace archives to the same hash. ?kind=diff returns the git diff HEAD of the repository in the workspace instead, when there was one. ?card_id=crd_… picks that card’s boundary; without it the newest one of the phase answers. Credential files (.env* and the platform’s own) and re-derivable caches such as node_modules are never archived. Needs audit:read, like stored proxy calls: it is captured content. A boundary with nothing stored — or a session in another organization — is 404 not_found.

Stream events

List and stream a session’s events, resumable by seq.