Agent Sessions GraphQL API
Administration and live monitoring for Agent workflow sessions: reading a session's transcript, sharing who may see and continue it, and watching sessions live without polling. Holding the conversation itself ā sending a turn, streaming an answer, deciding a tool approval ā goes over the Agent Responses API instead; this page covers everything alongside it.
Session Ownershipā
Every agent session has an owner scope that governs who may see it, continue it, decide its approvals, and watch it live ā on every surface, not just GraphQL.
| Scope | Who can see the session |
|---|---|
User | The session's starter (userId) only. |
Division | Anyone whose accessible divisions (primary, additional, or a descendant) include the owning division, whatever their VisibleTransactions setting. |
Organization | Any member of the organization. |
A system administrator can always see every session, regardless of scope.
Defaults at creation. A chat session (someone talking to an agent over the Responses API) starts User-owned by its starter. A task session (an agent run from a workflow, with no person present) starts Organization-owned ā it has no starter to default to.
Where it appliesā
| Surface | Behaviour for a session the caller may not see |
|---|---|
agentSession query | null, as if it does not exist |
agentSessions query | Excluded (and from totalCount) |
Continuing a response (previous_response_id) | 404 response_not_found ā no existence leak |
| Deciding an approval | Same as continuing: anyone who may see a session may continue it and decide its approvals |
onAgentSessionEvent | The event is not delivered |
Queriesā
agentSessionā
Fetch one session, including its transcript.
query Session($org: Int!, $id: UUID!) {
agentSession(organizationId: $org, agentSessionId: $id) {
agentSessionId workflowId userId sessionType status
lastResponseId ownerScope ownerDivisionId
turnCount promptTokens completionTokens
createdAt updatedAt completedAt
transcript
}
}
Returns null when the session doesn't exist or the caller may not see it under Session Ownership ā the two look identical to a caller with no business knowing which.
agentSessionsā
Offset-paged list, scoped to an organization and optionally a workflow.
query Sessions($org: Int!, $workflowId: UUID) {
agentSessions(organizationId: $org, workflowId: $workflowId, orderBy: "-createdAt", take: 20) {
totalCount
items { agentSessionId status sessionType ownerScope createdAt updatedAt }
}
}
Standard offset paging (skip, take), the standard filter string, and orderBy (default -createdAt). Sessions the caller may not see are excluded, including from totalCount. Select transcript only when you need it ā including it loads every message of every session on the page.
Parametersā
| Parameter | Type | Required | Description |
|---|---|---|---|
organizationId | Int | Yes | Organization scope. |
agentSessionId | UUID | agentSession only | The session to fetch. |
workflowId | UUID | No | Restrict agentSessions to one Agent workflow. |
filter | String | No | Lucene filter syntax. |
orderBy | String | No | Sort field; default -createdAt. |
skip, take | Int | No | Offset paging. |
The lastResponseId fieldā
lastResponseId is the id of the most recent completed response ā what a client resumes from. It's what an approvals inbox needs to act on a paused session: select it on agentSession or on an agentSessions item, then send decisions to the Responses API with it as previous_response_id.
Transcript documentā
transcript is the full audit record: every message the session ever wrote, including ones later summarized away or left over from an abandoned response.
{
"sessionId": "ā¦",
"status": "AwaitingApproval",
"messages": [
{ "role": "system", "content": [ { "type": "text", "text": "ā¦" } ] },
{ "role": "user", "content": [ { "type": "text", "text": "Cancel ORD-1" } ] },
{ "role": "assistant", "content": [ { "type": "toolCall", "callId": "call_abc", "name": "Sessions_E2E_Cancel_Shipment", "arguments": { "orderNumber": "ORD-1" } } ] },
{ "role": "tool", "content": [ { "type": "toolResult", "callId": "call_abc", "result": "\"Shipment ORD-1 was cancelled\"",
"approval": { "approved": true, "by": "<userId>", "at": "2026-09-23T12:00:00.0000000Z" } } ] },
{ "role": "summary", "summarizesThroughSequence": 24, "content": [ { "type": "text", "text": "ā¦" } ] }
],
"usage": [
{ "turn": 1, "promptTokens": 812, "completionTokens": 40 },
{ "turn": 2, "promptTokens": 5393, "completionTokens": 3965, "kind": "summary" }
],
"retries": [],
"failure": "ā¦"
}
- Content blocks:
text;toolCall(callId,name,argumentsobject);toolResult(callId,resultas a JSON string, plusapprovalwhen a person decided the call). - A
summarymessage marks where older history was compressed;summarizesThroughSequenceis the last message it replaced. Render it as a divider, not a chat bubble. failureis present only when the session failed.- A call waiting for approval is a
toolCallwith no matchingtoolResultyet.
Mutationsā
setAgentSessionOwnerā
Changes who may see, continue, and watch a session ā the starter or a system administrator only.
mutation ShareSession($input: SetAgentSessionOwnerInput!) {
setAgentSessionOwner(input: $input) {
agentSession { agentSessionId ownerScope ownerDivisionId }
}
}
{
"input": {
"organizationId": 42,
"agentSessionId": "9c3eā¦",
"scope": "Division",
"divisionId": 7
}
}
Input fields:
| Field | Type | Required | Description |
|---|---|---|---|
organizationId | Int | Yes | Organization scope. |
agentSessionId | UUID | Yes | The session to reassign. |
scope | AgentSessionOwnerScope (User | Division | Organization) | Yes | The new owner scope. |
divisionId | Int | With scope: Division | The division to own the session. |
Access rulesā
| Situation | Result |
|---|---|
| The caller may not see the session | Not found ā the same response as a missing session, so nothing about it leaks to someone with no business knowing it exists. |
| The caller can see the session but is neither its starter nor a system administrator | Forbidden; nothing changes. |
scope: Division without divisionId, or with a division outside the organization or outside the caller's accessible divisions | Validation error. A system administrator may choose any division of the organization. |
scope: User on a session with no starter (a task run) | Validation error ā a session with no starter cannot be shared with a user. |
scope: User always means the session's own starter, never the caller making the request. On success, the session's ownerScope/ownerDivisionId are updated and an OwnerChanged event is published to onAgentSessionEvent.
Subscriptionsā
onAgentSessionEventā
Coarse, live events for every session the caller may watch, so an approvals inbox, a monitoring view, another open tab, or a task session (which has no HTTP stream of its own) can follow a session without polling. Token-level text deltas are never published here ā those stay on the Responses API's SSE stream.
subscription {
onAgentSessionEvent(organizationId: 42, agentSessionId: "ā¦", workflowId: "ā¦") {
type agentSessionId workflowId sessionType status
ownerScope ownerDivisionId userId lastResponseId occurredAt
approval { requestId callId name arguments argumentsTruncated }
toolCall { callId name error }
compression { throughSequence }
}
}
agentSessionId and workflowId are optional filters; omit both to watch every session of the organization the caller may see. Every event carries the session's identity, sessionType, current status, owner fields, userId, lastResponseId, and occurredAt (UTC) ā enough to render or refresh a row without a follow-up query.
Eventsā
type | When | Payload |
|---|---|---|
StatusChanged | Session created; ā Running (including a resumed pause); ā AwaitingApproval; Completed; Exhausted; TimedOut; Failed; Cancelled | status, lastResponseId |
ApprovalRequested | A chat pauses ā one event per waiting call | approval: requestId (mcpr_<callId>), callId, name, arguments (a JSON string, cut to 1,024 characters), argumentsTruncated |
ToolCallCompleted | A tool result is stored ā live turns and resumed decisions; declined and refused calls included | toolCall: callId, name, error (set when failed, declined, or refused) |
HistoryCompressed | A summary row is stored | compression.throughSequence |
OwnerChanged | setAgentSessionOwner succeeds | the new ownerScope, ownerDivisionId |
StatusChanged is the only authority on a session's status. ApprovalRequested and ToolCallCompleted are published as their rows are stored, before the turn's terminal save, so they carry whatever status and lastResponseId the session had when the turn started ā Running for a new session or a resumed pause, but Completed for a follow-up turn of a chat that had already completed. Take the new response id from the StatusChanged(AwaitingApproval) event that follows ApprovalRequested, not from the request event itself.
Delivery rulesā
- An event is published only after the write it describes has succeeded.
- Best effort and live only ā no replay, and no ordering guarantee across API instances. Load current state through the queries above on connect and reconnect, then apply events from there.
- The resolver delivers an event only when the caller may see it under Session Ownership, evaluated fresh against the ownership the database holds as the event is sent ā never cached across events. An ownership change applies from the next event onward, including later events of a run already in progress.
- Postgres
NOTIFYlimits a message to 8,000 bytes;argumentsis cut to 1,024 characters withargumentsTruncatedset when it was. The full call is always in the transcript.
Authentication and accessā
Subscribing to an organization the caller does not belong to is rejected at subscribe time, before any stream opens, with a GraphQL error whose extensions.code is FORBIDDEN.
Authentication is the same bearer token as everywhere else, sent in the connection_init payload:
{ "Authorization": "Bearer <token>" }
Approval Notificationsā
When a chat session pauses for a tool approval, the person who started it gets a notification (type: TaskAssignment, delivered on both the Web and Push channels) so they don't have to be watching the conversation. It links back to the session with:
| Field | Value |
|---|---|
entityType | "AgentSession" |
entityId | The session's agentSessionId, as a string GUID |
A task session (no starter) gets no notification ā there's nobody to notify.
Breaking Change: Notification.entityId is now a Stringā
Notification.entityId ā on the notification type, and on createNotification's input ā changed from Int to String so a notification can link to any entity, including one like AgentSession whose id is a GUID rather than an integer. A client that parses entityId as a number, or sends an integer literal for it, needs to change to string handling. See Notifications GraphQL API for the full type.