Skip to main content

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.

ScopeWho can see the session
UserThe session's starter (userId) only.
DivisionAnyone whose accessible divisions (primary, additional, or a descendant) include the owning division, whatever their VisibleTransactions setting.
OrganizationAny 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​

SurfaceBehaviour for a session the caller may not see
agentSession querynull, as if it does not exist
agentSessions queryExcluded (and from totalCount)
Continuing a response (previous_response_id)404 response_not_found — no existence leak
Deciding an approvalSame as continuing: anyone who may see a session may continue it and decide its approvals
onAgentSessionEventThe 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​

ParameterTypeRequiredDescription
organizationIdIntYesOrganization scope.
agentSessionIdUUIDagentSession onlyThe session to fetch.
workflowIdUUIDNoRestrict agentSessions to one Agent workflow.
filterStringNoLucene filter syntax.
orderByStringNoSort field; default -createdAt.
skip, takeIntNoOffset 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, arguments object); toolResult (callId, result as a JSON string, plus approval when a person decided the call).
  • A summary message marks where older history was compressed; summarizesThroughSequence is the last message it replaced. Render it as a divider, not a chat bubble.
  • failure is present only when the session failed.
  • A call waiting for approval is a toolCall with no matching toolResult yet.

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:

FieldTypeRequiredDescription
organizationIdIntYesOrganization scope.
agentSessionIdUUIDYesThe session to reassign.
scopeAgentSessionOwnerScope (User | Division | Organization)YesThe new owner scope.
divisionIdIntWith scope: DivisionThe division to own the session.

Access rules​

SituationResult
The caller may not see the sessionNot 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 administratorForbidden; nothing changes.
scope: Division without divisionId, or with a division outside the organization or outside the caller's accessible divisionsValidation 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​

typeWhenPayload
StatusChangedSession created; → Running (including a resumed pause); → AwaitingApproval; Completed; Exhausted; TimedOut; Failed; Cancelledstatus, lastResponseId
ApprovalRequestedA chat pauses — one event per waiting callapproval: requestId (mcpr_<callId>), callId, name, arguments (a JSON string, cut to 1,024 characters), argumentsTruncated
ToolCallCompletedA tool result is stored — live turns and resumed decisions; declined and refused calls includedtoolCall: callId, name, error (set when failed, declined, or refused)
HistoryCompressedA summary row is storedcompression.throughSequence
OwnerChangedsetAgentSessionOwner succeedsthe 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 NOTIFY limits a message to 8,000 bytes; arguments is cut to 1,024 characters with argumentsTruncated set 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:

FieldValue
entityType"AgentSession"
entityIdThe 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.