API Reference - Sessions
Sessions represent a long-running agent or graph execution attached to a workspace. Each session progresses through a defined lifecycle and can park mid-turn to await operator input via yielding-tool endpoints.
Session lifecycle, statuses, and the session detail view.
Endpoints
| Method | Path | Summary |
|---|---|---|
| POST | /v1/workspaces/{workspace_id}/sessions |
Create a session |
| GET | /v1/workspaces/{workspace_id}/sessions |
List sessions on a workspace |
| GET | /v1/workspaces/{workspace_id}/sessions/{session_id} |
Get session state |
| DELETE | /v1/workspaces/{workspace_id}/sessions/{session_id} |
Hard-delete a session |
| POST | /v1/workspaces/{workspace_id}/sessions/{session_id}/resume |
Start or resume (idempotent) |
| POST | /v1/workspaces/{workspace_id}/sessions/{session_id}/pause |
Soft pause request |
| POST | /v1/workspaces/{workspace_id}/sessions/{session_id}/cancel |
Hard cancel to ENDED |
| POST | /v1/workspaces/{workspace_id}/sessions/{session_id}/steer |
Append a steering instruction |
| GET | /v1/sessions |
List sessions across all workspaces |
| GET | /v1/sessions/{session_id} |
Get session by id (no workspace context) |
| GET | /v1/sessions/{session_id}/messages |
Read the recorded message history for any status |
| POST | /v1/sessions/find |
Predicate-based search with cursor or offset paging |
| GET | /v1/sessions/{session_id}/ask_user/pending |
Get pending ask_user prompt |
| POST | /v1/sessions/{session_id}/ask_user/respond |
Submit response to ask_user |
| POST | /v1/sessions/{session_id}/yields/{tool_call_id}/cancel |
Cancel one in-flight yield |
| GET | /v1/sessions/{session_id}/tool_approval/pending |
Get pending tool approval request |
| POST | /v1/sessions/{session_id}/tool_approval/respond |
Submit an approval decision |
POST /v1/workspaces/{workspace_id}/sessions
Creates a session and optionally starts it immediately.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
binding |
object | yes | Discriminated union: {"kind":"agent","agent_id":"..."} or {"kind":"graph","graph_id":"..."} |
auto_start |
boolean | no | Default false. When true, transitions immediately to running |
initial_instructions |
string | no | Prepended to the first turn as a user instruction |
parent_session_id |
string | no | Links this session as a child for hierarchical filtering |
metadata |
object | no | Arbitrary key-value blob stored on the row |
graph_input |
any | no | Input value for graph bindings only |
curl -X POST https://primer.example/v1/workspaces/ws-prod/sessions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"binding": {"kind": "agent", "agent_id": "agent-coder"},
"auto_start": true,
"initial_instructions": "Implement the described feature"
}'import httpx
r = httpx.post(
"https://primer.example/v1/workspaces/ws-prod/sessions",
headers={"Authorization": f"Bearer {token}"},
json={
"binding": {"kind": "agent", "agent_id": "agent-coder"},
"auto_start": True,
"initial_instructions": "Implement the described feature",
},
)
r.raise_for_status()
session = r.json()const r = await fetch("/v1/workspaces/ws-prod/sessions", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
binding: { kind: "agent", agent_id: "agent-coder" },
auto_start: true,
initial_instructions: "Implement the described feature",
}),
});
const session = await r.json();Response: 201
Returns a WorkspaceSession object. Key fields:
| Field | Type | Notes |
|---|---|---|
id |
string | Server-assigned session id |
workspace_id |
string | Parent workspace |
binding |
object | Echoes the supplied binding with kind, agent_id or graph_id |
status |
string | created, running, waiting, paused, or ended |
turn_no |
integer | Increments on each turn |
parked_status |
string or null | parked, resumable, or null |
ended_reason |
string or null | completed, failed, cancelled, workspace_lost, force_deleted |
ended_at |
string or null | ISO-8601 timestamp when the session ended |
metadata |
object | Echoed from create body |
created_at |
string | ISO-8601 creation timestamp |
Errors: 404 workspace not found, 409 conflict (e.g. workspace in wrong state), 422 validation.
GET /v1/workspaces/{workspace_id}/sessions
Lists sessions on a specific workspace. Supports offset pagination via limit and offset query params.
Query parameters: limit, offset, order_by (e.g. created_at:desc).
Response: 200 - {"items": [WorkspaceSession, ...], "total": N}
GET /v1/sessions
Lists sessions across all workspaces with optional filtering.
Query parameters:
| Param | Type | Notes |
|---|---|---|
workspace_id |
string | Filter to one workspace |
agent_id |
string | Filter to sessions with a matching agent binding; graph-bound sessions are never returned |
graph_id |
string | Filter to sessions with a matching graph binding; agent-bound sessions are never returned |
status |
string | One of created, running, waiting, paused, ended |
parent_session_id |
string | Direct-parent filter (non-transitive) |
order_by |
string | e.g. created_at:asc or created_at:desc |
limit |
integer | Page size |
offset |
integer | Page offset |
All filter params combine with AND semantics. A missing graph or agent produces an empty list, not a 404.
curl "https://primer.example/v1/sessions?workspace_id=ws-prod&status=running&limit=20&offset=0" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/sessions",
headers={"Authorization": f"Bearer {token}"},
params={"workspace_id": "ws-prod", "status": "running", "limit": 20, "offset": 0},
)
r.raise_for_status()
page = r.json()const params = new URLSearchParams({
workspace_id: "ws-prod", status: "running", limit: 20, offset: 0,
});
const r = await fetch(`/v1/sessions?${params}`, {
headers: { "Authorization": `Bearer ${token}` },
});
const page = await r.json();Response: 200 - {"items": [WorkspaceSession, ...], "total": N}
GET /v1/sessions/{session_id}
Fetches a single session by id without requiring a workspace URL prefix. Returns 404 if not found.
curl "https://primer.example/v1/sessions/sess-abc123" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/sessions/sess-abc123",
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()
session = r.json()const r = await fetch("/v1/sessions/sess-abc123", {
headers: { "Authorization": `Bearer ${token}` },
});
const session = await r.json();Response: 200 - WorkspaceSession object. Errors: 404 not found.
GET /v1/sessions/{session_id}/messages
Reads the session's recorded message history straight from its messages.jsonl log. Unlike the WebSocket transcript stream (which rejects an ended session), this endpoint serves history for a session in any status, including ended, so a tool can fetch the full transcript after the run is over. When the session's workspace or its log file is gone, the endpoint returns an empty log rather than a 5xx.
Query parameters:
| Param | Type | Notes |
|---|---|---|
limit |
integer | Page size, 1-1000, default 200 |
offset |
integer | Number of leading rows to skip |
after_seq |
integer | Return only rows whose sequence number is greater than this value |
curl "https://primer.example/v1/sessions/sess-abc123/messages?limit=200&offset=0" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/sessions/sess-abc123/messages",
headers={"Authorization": f"Bearer {token}"},
params={"limit": 200, "offset": 0},
)
r.raise_for_status()
page = r.json()const params = new URLSearchParams({ limit: 200, offset: 0 });
const r = await fetch(`/v1/sessions/sess-abc123/messages?${params}`, {
headers: { "Authorization": `Bearer ${token}` },
});
const page = await r.json();Response: 200 - {"items": [...], "total": N, "offset": N, "limit": N}, where each item is a recorded row from the session's messages.jsonl. A session whose workspace or log file is gone returns an empty items list (not a 5xx).
POST /v1/sessions/find
Predicate-based search with cursor or offset pagination and optional ordering.
Request body
curl -X POST "https://primer.example/v1/sessions/find" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"predicate": {
"kind": "predicate",
"op": "=",
"left": {"kind": "field", "name": "workspace_id"},
"right": {"kind": "value", "value": "ws-prod"}
},
"page": {"kind": "offset", "offset": 0, "length": 25},
"order_by": [{"field": "created_at", "direction": "desc"}]
}'import httpx
r = httpx.post(
"https://primer.example/v1/sessions/find",
headers={"Authorization": f"Bearer {token}"},
json={
"predicate": {
"kind": "predicate",
"op": "=",
"left": {"kind": "field", "name": "workspace_id"},
"right": {"kind": "value", "value": "ws-prod"},
},
"page": {"kind": "offset", "offset": 0, "length": 25},
"order_by": [{"field": "created_at", "direction": "desc"}],
},
)
r.raise_for_status()
result = r.json()const r = await fetch("/v1/sessions/find", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
predicate: {
kind: "predicate", op: "=",
left: { kind: "field", name: "workspace_id" },
right: { kind: "value", value: "ws-prod" },
},
page: { kind: "offset", offset: 0, length: 25 },
order_by: [{ field: "created_at", direction: "desc" }],
}),
});
const result = await r.json();Cursor mode: pass "page": {"kind": "cursor", "cursor": null, "length": 25} for the first page. Subsequent pages use cursor from the next_cursor field in the response. Cursor pagination is stable and complete: every matching session appears exactly once across the full walk, even when multiple sessions share the same timestamp. The cursor always includes the session id as a stable tiebreaker, so rows with identical sort-key values are never skipped or duplicated at page boundaries.
Response: 200 - For offset pages: {"kind": "offset", "items": [...], "total": N}. For cursor pages: {"kind": "cursor", "items": [...], "next_cursor": "..."} (null next_cursor means last page).
POST /v1/workspaces/{workspace_id}/sessions/{session_id}/resume
Starts or resumes a session. Idempotent: calling resume on an already-running session returns 200 with the current row unchanged. Returns 409 when called on a terminal (ended) session.
No request body required.
Response: 200 - WorkspaceSession with status: "running". Errors: 404 not found, 409 session is ended.
POST /v1/workspaces/{workspace_id}/sessions/{session_id}/pause
Requests a soft pause. The worker honours it at the next safe checkpoint.
No request body required.
Response: 200 - Updated WorkspaceSession. Errors: 404 not found, 409 session is ended.
POST /v1/workspaces/{workspace_id}/sessions/{session_id}/cancel
Hard-cancels a session. Transitions CREATED sessions immediately to ended/cancelled. Returns 409 on a session already in a terminal state.
No request body required.
Response: 200 - WorkspaceSession with status: "ended" and ended_reason: "cancelled".
curl -X POST "https://primer.example/v1/workspaces/ws-prod/sessions/sess-abc123/cancel" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.post(
"https://primer.example/v1/workspaces/ws-prod/sessions/sess-abc123/cancel",
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status()
session = r.json()const r = await fetch(
"/v1/workspaces/ws-prod/sessions/sess-abc123/cancel",
{ method: "POST", headers: { "Authorization": `Bearer ${token}` } },
);
const session = await r.json();POST /v1/workspaces/{workspace_id}/sessions/{session_id}/steer
Appends a steering instruction as a user-role message to the running session's transcript.
Request body: {"instruction": "Focus on the authentication module"}
Response: 200 - The instruction object.
GET /v1/sessions/{session_id}/ask_user/pending
Returns the current ask_user prompt when the session is parked waiting for operator input. Returns 404 when the session is not parked on ask_user.
curl "https://primer.example/v1/sessions/sess-abc123/ask_user/pending" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/sessions/sess-abc123/ask_user/pending",
headers={"Authorization": f"Bearer {token}"},
)const r = await fetch("/v1/sessions/sess-abc123/ask_user/pending", {
headers: { "Authorization": `Bearer ${token}` },
});Response: 200
| Field | Type | Notes |
|---|---|---|
tool_call_id |
string | Required for the respond call |
prompt |
string | Text the agent sent to the operator |
parked_at |
string | ISO-8601 timestamp when the session parked |
response_schema |
object or null | Optional JSON Schema the response must conform to |
Errors: 404 no pending ask_user prompt.
POST /v1/sessions/{session_id}/ask_user/respond
Submits the operator's reply to a pending ask_user prompt and resumes the session.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
tool_call_id |
string | yes | Must match the value from /pending |
response |
any | yes | String, object, array, number, or boolean; validated against response_schema when present |
curl -X POST "https://primer.example/v1/sessions/sess-abc123/ask_user/respond" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool_call_id": "tc-001", "response": "Use the OAuth2 flow"}'import httpx
r = httpx.post(
"https://primer.example/v1/sessions/sess-abc123/ask_user/respond",
headers={"Authorization": f"Bearer {token}"},
json={"tool_call_id": "tc-001", "response": "Use the OAuth2 flow"},
)
r.raise_for_status()const r = await fetch("/v1/sessions/sess-abc123/ask_user/respond", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ tool_call_id: "tc-001", response: "Use the OAuth2 flow" }),
});Response: 202 - {"status": "accepted"}. Errors: 404 no matching pending prompt.
POST /v1/sessions/{session_id}/yields/{tool_call_id}/cancel
Cancels one in-flight yield by tool_call_id, causing the agent to receive a YieldCancelled result on its next turn.
Request body: {"reason": "operator skipped"} (optional; reason may be null or omitted)
Response: 202 - {"status": "accepted"}. Errors: 404 session or yield not found.
GET /v1/sessions/{session_id}/tool_approval/pending
Returns the pending tool approval request when the session is parked on the _approval tool. Returns 404 if the session is not parked on _approval (including when parked on a different tool such as ask_user).
curl "https://primer.example/v1/sessions/sess-abc123/tool_approval/pending" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/sessions/sess-abc123/tool_approval/pending",
headers={"Authorization": f"Bearer {token}"},
)const r = await fetch("/v1/sessions/sess-abc123/tool_approval/pending", {
headers: { "Authorization": `Bearer ${token}` },
});Response: 200
| Field | Type | Notes |
|---|---|---|
tool_call_id |
string | Required for the respond call |
tool_name |
string | The inner tool name the agent tried to call |
arguments |
object | Arguments passed to the inner tool |
parked_at |
string | ISO-8601 timestamp |
timeout_at |
string or null | ISO-8601 expiry derived from parked_at + policy timeout |
policy_id |
string or null | Approval policy that triggered the gate |
approval_type |
string or null | e.g. required |
gate_reason |
string or null | Human-readable reason from the policy |
Errors: 404 not parked on _approval.
POST /v1/sessions/{session_id}/tool_approval/respond
Submits an approval decision for the pending tool call and resumes the session.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
tool_call_id |
string | yes | Must match the value from /pending |
decision |
string | yes | approved or rejected |
reason |
string | no | Optional free-text reason (max 1024 chars) |
curl -X POST "https://primer.example/v1/sessions/sess-abc123/tool_approval/respond" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tool_call_id": "tc-shell-001", "decision": "approved", "reason": "operator confirmed"}'import httpx
r = httpx.post(
"https://primer.example/v1/sessions/sess-abc123/tool_approval/respond",
headers={"Authorization": f"Bearer {token}"},
json={
"tool_call_id": "tc-shell-001",
"decision": "approved",
"reason": "operator confirmed",
},
)
r.raise_for_status()const r = await fetch("/v1/sessions/sess-abc123/tool_approval/respond", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
tool_call_id: "tc-shell-001",
decision: "approved",
reason: "operator confirmed",
}),
});Response: 202 - {"status": "accepted"}. Errors: 404 if tool_call_id does not match the parked yield (the parked id is never echoed in error responses).
Error envelopes
All error responses use RFC 7807 problem details:
# Example 404 response body
# {
# "type": "/errors/not-found",
# "title": "Not Found",
# "status": 404,
# "detail": "session not found"
# }# Catch via r.raise_for_status() or inspect r.json()["type"]:
# "/errors/not-found" : resource does not exist
# "/errors/conflict" : illegal state transition (e.g. resume on ended)
# "/errors/validation-error" : request body failed schema validation// On error, r.ok === false and r.json() returns:
// { type: "/errors/not-found", title: "...", status: 404, detail: "..." }
// Common types: /errors/not-found, /errors/conflict, /errors/validation-error