From 2f33c665da3f4126b95b1381fe24bba0138c40ad Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 14 Jun 2026 12:09:54 +0100 Subject: [PATCH 1/2] docs: document the Sessions HTTP API (reference, channels, scopes) Adds the Sessions API to the formal API reference: create/list/retrieve/update/close (OpenAPI spec + management/sessions pages), a reference page for the .in/.out realtime channel HTTP endpoints, and a session-scopes section in the authentication docs. Cross-linked with the conceptual ai-chat/sessions page. Documents the shipped HTTP contract for non-SDK and server-to-server callers. --- docs/ai-chat/sessions.mdx | 8 +- docs/docs.json | 11 + docs/management/authentication.mdx | 33 ++ docs/management/sessions/channels.mdx | 128 ++++++ docs/management/sessions/close.mdx | 4 + docs/management/sessions/create.mdx | 4 + docs/management/sessions/list.mdx | 4 + docs/management/sessions/retrieve.mdx | 4 + docs/management/sessions/update.mdx | 4 + docs/v3-openapi.yaml | 586 +++++++++++++++++++++++++- 10 files changed, 780 insertions(+), 6 deletions(-) create mode 100644 docs/management/sessions/channels.mdx create mode 100644 docs/management/sessions/close.mdx create mode 100644 docs/management/sessions/create.mdx create mode 100644 docs/management/sessions/list.mdx create mode 100644 docs/management/sessions/retrieve.mdx create mode 100644 docs/management/sessions/update.mdx diff --git a/docs/ai-chat/sessions.mdx b/docs/ai-chat/sessions.mdx index d5ec3abc4ca..ba012891e2d 100644 --- a/docs/ai-chat/sessions.mdx +++ b/docs/ai-chat/sessions.mdx @@ -197,7 +197,7 @@ The two channels mirror the producer/consumer pair in `streams.define` (out) and ## `session.out` — task → clients -The output channel. The task writes; external clients (browser, server action, another task) read via SSE. +The output channel. The task writes; external clients (browser, server action, another task) read via SSE. The underlying HTTP endpoints are documented in [Session channels](/management/sessions/channels) for non-SDK callers. ### `out.append(value, options?)` @@ -246,7 +246,7 @@ Append an S2 `trim` command. Records with `seq_num < earliestSeqNum` are eventua ## `session.in` — clients → task -The input channel. External clients call `send`; the task consumes via `on` / `once` / `peek` / `wait` / `waitWithIdleTimeout`. +The input channel. External clients call `send`; the task consumes via `on` / `once` / `peek` / `wait` / `waitWithIdleTimeout`. The underlying HTTP endpoints are documented in [Session channels](/management/sessions/channels) for non-SDK callers. ### `in.send(value, requestOptions?)` @@ -319,8 +319,12 @@ Tokens authorize **both** URL forms: `/sessions/{externalId}/...` and `/sessions For the `chat.agent` transport, `auth.createPublicToken` is wrapped by `accessToken` in `useTriggerChatTransport`; for direct session access from your server, mint a token per request just like any other realtime resource. +See [Session scopes](/management/authentication#session-scopes) for exactly what `read:sessions` and `write:sessions` grant, and why updating, closing, and appending to `.out` require a secret key. + ## See also +- [Sessions HTTP API](/management/sessions/create) — The REST endpoints for creating, listing, retrieving, updating, and closing sessions, plus the [channel endpoints](/management/sessions/channels) for non-SDK callers. +- [Session scopes](/management/authentication#session-scopes) — The public-token scopes that authorize session and channel access. - [How it works](/ai-chat/how-it-works) — How `chat.agent` builds on Sessions. - [Backend](/ai-chat/backend) — `chat.agent` / `chat.createSession` / raw `task()` with chat primitives. - [Client Protocol](/ai-chat/client-protocol) — The wire-level view of `.in/append` and `.out` SSE. diff --git a/docs/docs.json b/docs/docs.json index e2cb83db046..367b99a9146 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -437,6 +437,17 @@ "management/waitpoints/complete-callback" ] }, + { + "group": "Sessions API", + "pages": [ + "management/sessions/create", + "management/sessions/list", + "management/sessions/retrieve", + "management/sessions/update", + "management/sessions/close", + "management/sessions/channels" + ] + }, { "group": "Query API", "pages": [ diff --git a/docs/management/authentication.mdx b/docs/management/authentication.mdx index ce85f23cb94..d1ac301b72f 100644 --- a/docs/management/authentication.mdx +++ b/docs/management/authentication.mdx @@ -189,3 +189,36 @@ Unlike `TriggerClient` instances (which stay isolated unless you opt in), `auth. concurrency. If you need concurrent multi-target calls there, use [`new TriggerClient({...})`](/management/multiple-clients) instances instead. + +## Session scopes + +[Sessions](/ai-chat/sessions) are addressed by a session-scoped public access token — a short-lived JWT you mint in your backend and pass to frontend or server-side clients. The token carries one or both of two scopes, each pinned to a session by its friendly ID (`session_…`) or your `externalId`: + +| Scope | Grants | +| --- | --- | +| `read:sessions:{id}` | Retrieve and list the session, and subscribe to and drain both its `.in` and `.out` [channels](/management/sessions/channels). | +| `write:sessions:{id}` | Append to the session's `.in` channel, and create runs on the session (including the create call itself). | + +Two boundaries follow from the table, and both are enforced server-side: + +- **`write:sessions` does not grant `.out` append.** The `.out` channel is the task's to write. Appending to `.out` requires a **secret key**; a public token gets `403`. +- **Updating or closing a session requires a secret key.** A session public token cannot call `PATCH /api/v1/sessions/{session}` or `POST /api/v1/sessions/{session}/close` — those are admin operations. + +Mint a token with `auth.createPublicToken` in your backend: + +```ts Your backend +import { auth } from "@trigger.dev/sdk"; + +const publicToken = await auth.createPublicToken({ + scopes: { + read: { sessions: "session_123" }, + write: { sessions: "session_123" }, + }, +}); +``` + +`sessions` accepts a single ID or an array. The default token TTL is 1 hour. One token authorizes **both** URL forms — pass either your `externalId` or the `session_…` ID in the path. + +The `publicAccessToken` returned by [`sessions.start()`](/management/sessions/create) already carries both scopes for the session it created, so you usually don't mint one by hand for the create flow. + +For the full channel HTTP surface these scopes authorize, see [Session channels](/management/sessions/channels). For the SDK side, see [Sessions](/ai-chat/sessions). For general public-token usage (expiration formats, trigger tokens, scoping to runs and tasks), see [Realtime authentication](/realtime/auth). diff --git a/docs/management/sessions/channels.mdx b/docs/management/sessions/channels.mdx new file mode 100644 index 00000000000..f8508eb8ce4 --- /dev/null +++ b/docs/management/sessions/channels.mdx @@ -0,0 +1,128 @@ +--- +title: "Session channels" +sidebarTitle: "Channels" +description: "The raw HTTP endpoints behind a session's .in and .out streams: append records, read them over SSE, and drain them non-streaming." +--- + +Every session has two durable streams: `.in` carries records from your clients to the task, `.out` carries records from the task back to your clients. The [`sessions` SDK](/ai-chat/sessions) wraps these as `session.in.*` and `session.out.*`. This page documents the underlying HTTP endpoints for callers that aren't using the TypeScript SDK. + +All channel endpoints live under `/realtime/v1/sessions/{session}/{io}`, where: + +- `{session}` is the session's friendly ID (`session_…`) or your `externalId`. One token authorizes both forms. +- `{io}` is either `in` or `out`. + +Authorize requests with a secret key or a [session public token](/management/authentication#session-scopes). The token's scopes decide what you can do — see [Authorization](#authorization) below. + +## Append a record + +Append a single record to a channel. + +```bash Append to .in +curl -X POST "https://api.trigger.dev/realtime/v1/sessions/{session}/in/append" \ + -H "Authorization: Bearer $TRIGGER_TOKEN" \ + -H "Content-Type: application/json" \ + -H "X-Part-Id: 0f8c2b1e-..." \ + --data '{"type":"user-message","text":"hello"}' +``` + +The body is the raw record — any text up to 1MiB (records over the per-record cap return `413`). The response is `{ "ok": true }`. + +Set the `X-Part-Id` header to a unique value per record to make the append idempotent: replaying the same `X-Part-Id` does not duplicate the record. Appending to a closed or expired session returns `400`. + + + Appending to `.out` requires a **secret key**. A session public token (even one with + `write:sessions`) can only append to `.in` — appending to `.out` with a public token returns + `403`. The `.out` stream is the task's to write. + + +## Read a channel over SSE + +Subscribe to a channel as a Server-Sent Events stream. New records are delivered as they arrive. + +```bash Read .out +curl -N "https://api.trigger.dev/realtime/v1/sessions/{session}/out" \ + -H "Authorization: Bearer $TRIGGER_TOKEN" \ + -H "Last-Event-ID: 42" \ + -H "Timeout-Seconds: 60" +``` + +| Header | Direction | Description | +| --- | --- | --- | +| `Last-Event-ID` | request | Resume after this sequence number. Set it to the last `id:` you received to pick up exactly where you left off after a disconnect. | +| `Timeout-Seconds` | request | How long the server holds the stream open with no new records before closing, `1`–`600`. | + +Each SSE event carries: + +- `id:` — the record's sequence number. Use the most recent one as `Last-Event-ID` to resume. +- `data:` — a JSON record `{ "data": , "id": }`. For `.out` on a `chat.agent` session, `data` is a UI message chunk (text, reasoning, tool call, or a custom data part). + +```text +id: 42 +data: {"data":{"type":"text","text":"echo: hello"},"id":42} +``` + +### Control records + +Some `.out` events are **control records** rather than data. A control record has an empty body and carries a `trigger-control` header naming its subtype: + +| Subtype | Meaning | +| --- | --- | +| `turn-complete` | The current turn finished. Carries sibling headers `public-access-token` (a refreshed session token), `session-in-event-id`, and `last-event-id`. | +| `upgrade-required` | The session needs to hand off to a run on a newer deployed version. | + +Route control records by their subtype instead of treating them as message content. The TypeScript SDK does this for you — `session.out.read` filters control records out of the chunk stream and surfaces them through `onControl`. + +## Drain records non-streaming + +Fetch a batch of records without holding an SSE connection open. Useful for polling or for reading a tail at startup. + +```bash Drain .out +curl "https://api.trigger.dev/realtime/v1/sessions/{session}/out/records?afterEventId=42" \ + -H "Authorization: Bearer $TRIGGER_TOKEN" +``` + +Pass `afterEventId` to return only records after that sequence number; omit it to read from the start of the retained window. The response is: + +```json +{ + "records": [ + { "data": { "type": "text", "text": "echo: hello" }, "id": 43, "seqNum": 43 } + ] +} +``` + +Each record carries `data`, `id`, `seqNum`, and an optional `headers` array (present on control records). Page forward by passing the highest `seqNum` you received as the next `afterEventId`. + +## Authorization + +The action you can take depends on your token and the channel: + +| Action | Endpoint | Required authorization | +| --- | --- | --- | +| Subscribe (SSE) | `GET .../{io}` | `read:sessions:{id}` — works on both `.in` and `.out` | +| Drain records | `GET .../{io}/records` | `read:sessions:{id}` — works on both `.in` and `.out` | +| Append to `.in` | `POST .../in/append` | `write:sessions:{id}` | +| Append to `.out` | `POST .../out/append` | Secret key only | + +Reads work in both directions for a `read:sessions` token. Writes split by direction: a `write:sessions` token can append to `.in`, but `.out` is reserved for the task and requires a secret key. See [session scopes](/management/authentication#session-scopes) for how to mint a token. + +## Using the SDK instead + +If you're writing TypeScript, the [`sessions` SDK](/ai-chat/sessions) is the ergonomic path. `sessions.open(idOrExternalId)` returns a `SessionHandle` whose `session.in` and `session.out` channels call these endpoints for you, with auto-retry, `Last-Event-ID` resume, and control-record routing built in: + +```ts Your backend +import { sessions } from "@trigger.dev/sdk"; + +const session = sessions.open(chatId); + +// append to .in +await session.in.send({ type: "user-message", text: "hello" }); + +// read .out over SSE +const stream = await session.out.read({ signal: AbortSignal.timeout(30_000) }); +for await (const chunk of stream) { + console.log(chunk); +} +``` + +See [`session.in`](/ai-chat/sessions#session-in-—-clients-→-task) and [`session.out`](/ai-chat/sessions#session-out-—-task-→-clients) for the full handle API. diff --git a/docs/management/sessions/close.mdx b/docs/management/sessions/close.mdx new file mode 100644 index 00000000000..a89d229c5d2 --- /dev/null +++ b/docs/management/sessions/close.mdx @@ -0,0 +1,4 @@ +--- +title: "Close session" +openapi: "v3-openapi POST /api/v1/sessions/{session}/close" +--- diff --git a/docs/management/sessions/create.mdx b/docs/management/sessions/create.mdx new file mode 100644 index 00000000000..f890c138286 --- /dev/null +++ b/docs/management/sessions/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create session" +openapi: "v3-openapi POST /api/v1/sessions" +--- diff --git a/docs/management/sessions/list.mdx b/docs/management/sessions/list.mdx new file mode 100644 index 00000000000..c3406c5f8b9 --- /dev/null +++ b/docs/management/sessions/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List sessions" +openapi: "v3-openapi GET /api/v1/sessions" +--- diff --git a/docs/management/sessions/retrieve.mdx b/docs/management/sessions/retrieve.mdx new file mode 100644 index 00000000000..23b1ab2c443 --- /dev/null +++ b/docs/management/sessions/retrieve.mdx @@ -0,0 +1,4 @@ +--- +title: "Retrieve session" +openapi: "v3-openapi GET /api/v1/sessions/{session}" +--- diff --git a/docs/management/sessions/update.mdx b/docs/management/sessions/update.mdx new file mode 100644 index 00000000000..0f77c0be172 --- /dev/null +++ b/docs/management/sessions/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update session" +openapi: "v3-openapi PATCH /api/v1/sessions/{session}" +--- diff --git a/docs/v3-openapi.yaml b/docs/v3-openapi.yaml index fa052e7c441..01ddb0fad0c 100644 --- a/docs/v3-openapi.yaml +++ b/docs/v3-openapi.yaml @@ -3257,6 +3257,263 @@ paths: console.log(token.output); } + "/api/v1/sessions": + post: + operationId: create_session_v1 + summary: Create a session + description: >- + Create a Session and trigger its first run in one atomic call. A Session is + the durable identity for a bi-directional stream of records (the `.in` and + `.out` channels) that survives across the runs processing it. + + + Idempotent on `externalId` within an environment. Calling create again with + an `externalId` that already maps to an open session returns the existing + session with `isCached: true` and `201` becomes `200`. Reusing an + `externalId` whose session is already closed or expired returns `409`. + + + Authorize with a secret key, or a public token carrying `write:sessions` for + the session you are creating. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/CreateSessionRequestBody" + responses: + "201": + description: Session created and its first run triggered. + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedSessionResponseBody" + "200": + description: >- + An open session already existed for this `externalId`. The existing + session is returned with `isCached: true`. + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedSessionResponseBody" + "401": + description: Unauthorized + "409": + description: >- + An `externalId` was reused, but its session is already closed or expired. + Closed and expired sessions cannot be reopened. + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorResponse" + "422": + description: >- + Validation failed — for example the request body exceeds 32KB, or + `externalId` starts with the reserved `session_` prefix. + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorWithDetailsResponse" + tags: + - sessions + security: + - secretKey: [] + - publicAccessToken: [] + x-codeSamples: + - lang: typescript + label: Start a session + source: |- + import { sessions } from "@trigger.dev/sdk"; + + const { id, runId, publicAccessToken, isCached } = await sessions.start({ + type: "chat.agent", + externalId: chatId, + taskIdentifier: "my-chat", + triggerConfig: { + basePayload: { chatId }, + tags: [`chat:${chatId}`], + }, + }); + + console.log(id); // e.g. "session_abc123" + console.log(runId); // the first run, e.g. "run_def456" + console.log(isCached); // false on a brand-new session + + get: + operationId: list_sessions_v1 + summary: List sessions + description: >- + List sessions in the current environment, newest first. Filter by type, tags, + task identifier, external id, status, and creation window. Use cursor-based + pagination with `page[after]` and `page[before]` to navigate pages. + + + List rows omit `triggerConfig`; retrieve a single session to read it. + parameters: + - $ref: "#/components/parameters/cursorPagination" + - $ref: "#/components/parameters/sessionsFilter" + responses: + "200": + description: Successful request + content: + application/json: + schema: + "$ref": "#/components/schemas/ListSessionsResult" + "400": + description: Invalid query parameters + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorWithDetailsResponse" + "401": + description: Unauthorized request + tags: + - sessions + security: + - secretKey: [] + - publicAccessToken: [] + x-codeSamples: + - lang: typescript + label: List sessions + source: |- + import { sessions } from "@trigger.dev/sdk"; + + for await (const session of sessions.list({ + type: "chat.agent", + status: "ACTIVE", + limit: 50, + })) { + console.log(session.id, session.externalId, session.createdAt); + } + + "/api/v1/sessions/{session}": + parameters: + - $ref: "#/components/parameters/session" + get: + operationId: retrieve_session_v1 + summary: Retrieve a session + description: >- + Retrieve a single session by its friendly id (`session_…`) or your + `externalId`. The response includes `triggerConfig` and the friendly + `currentRunId` of the live run, if any. + responses: + "200": + description: Successful request + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionObject" + "401": + description: Unauthorized request + "404": + description: Session not found + tags: + - sessions + security: + - secretKey: [] + - publicAccessToken: [] + x-codeSamples: + - lang: typescript + label: Retrieve a session + source: |- + import { sessions } from "@trigger.dev/sdk"; + + const session = await sessions.retrieve(chatId); + + console.log(session.currentRunId, session.tags, session.closedAt); + + patch: + operationId: update_session_v1 + summary: Update a session + description: >- + Update a session's `tags` or `metadata`. Pass `metadata: null` to clear it. + + + Requires a secret key — a session public token cannot update a session. + `externalId` is read-only after create: sending a different value returns + `422`; sending the same value (or `null` to clear) is accepted. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/UpdateSessionRequestBody" + responses: + "200": + description: Session updated successfully + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionObject" + "401": + description: Unauthorized request + "404": + description: Session not found + "422": + description: >- + Validation failed — for example an attempt to change `externalId` to a + different value, or a body exceeding 32KB. + content: + application/json: + schema: + "$ref": "#/components/schemas/ErrorWithDetailsResponse" + tags: + - sessions + security: + - secretKey: [] + x-codeSamples: + - lang: typescript + label: Update a session + source: |- + import { sessions } from "@trigger.dev/sdk"; + + const session = await sessions.update(chatId, { + tags: ["priority"], + metadata: { lastSeenBy: "support" }, + }); + + "/api/v1/sessions/{session}/close": + parameters: + - $ref: "#/components/parameters/session" + post: + operationId: close_session_v1 + summary: Close a session + description: >- + Close a session. Closing is terminal and idempotent — closing an + already-closed session returns the existing row unchanged. A closed session + cannot be reopened, and reusing its `externalId` on create returns `409`. + + + Requires a secret key — a session public token cannot close a session. + requestBody: + required: false + content: + application/json: + schema: + "$ref": "#/components/schemas/CloseSessionRequestBody" + responses: + "200": + description: Session closed successfully. Returns the session row. + content: + application/json: + schema: + "$ref": "#/components/schemas/SessionObject" + "401": + description: Unauthorized request + "404": + description: Session not found + tags: + - sessions + security: + - secretKey: [] + x-codeSamples: + - lang: typescript + label: Close a session + source: |- + import { sessions } from "@trigger.dev/sdk"; + + await sessions.close(chatId, { reason: "user signed out" }); + components: parameters: taskIdentifier: @@ -3361,6 +3618,29 @@ components: type: string description: The name of the environment variable. example: SLACK_API_KEY + session: + in: path + name: session + required: true + schema: + type: string + description: >- + The session's friendly ID (`session_…`) or your `externalId`. The server + disambiguates by the `session_` prefix. + example: session_abc123 + sessionsFilter: + in: query + name: filter + style: deepObject + explode: true + description: | + Use this parameter to filter the sessions. You can filter by type, tags, task identifier, external id, status, and created at. + + For array fields, you can provide multiple values to filter by using a comma-separated list. For example, to get ACTIVE and CLOSED sessions, you can use `filter[status]=ACTIVE,CLOSED`. + + For object fields, you should use the "form" encoding style. For example, to filter by the period, you can use `filter[createdAt][period]=1d`. + schema: + $ref: "#/components/schemas/SessionsFilter" securitySchemes: secretKey: type: http @@ -3396,11 +3676,13 @@ components: type: http scheme: bearer description: | - A short-lived JWT scoped to a specific waitpoint token. Returned as `publicAccessToken` - when you call `wait.createToken()` or `POST /api/v1/waitpoints/tokens`. + A short-lived JWT scoped to specific resources, returned as `publicAccessToken` from APIs + such as `wait.createToken()` / `POST /api/v1/waitpoints/tokens` and + `sessions.start()` / `POST /api/v1/sessions`, or minted directly with `auth.createPublicToken()`. - This token is safe to embed in frontend clients — it can only complete the specific - waitpoint it was issued for and cannot be used for any other API operations. + This token is safe to embed in frontend clients — it can only act on the resources its + scopes grant (e.g. `read:sessions:{id}`, `write:sessions:{id}`) and cannot be used for any + other API operations. For session tokens see the [session scopes](/management/authentication#session-scopes). schemas: RunTag: @@ -3895,6 +4177,302 @@ components: example: 491 description: The duration of compute (so far) in milliseconds. This does not include waits. + SessionTriggerConfig: + type: object + description: >- + Trigger options applied to every run a session schedules. `basePayload` is the + wire payload merged into each run; the remaining fields map onto the standard + trigger options. + required: + - basePayload + properties: + basePayload: + type: object + additionalProperties: true + description: >- + Base payload passed to every run this session triggers. For `chat.agent` + this carries `{ chatId, ...clientData }`. + machine: + type: string + description: Machine preset for each run, e.g. `small-1x`. + example: small-1x + queue: + type: string + maxLength: 128 + description: Queue to schedule runs on. + tags: + type: array + maxItems: 5 + items: + type: string + maxLength: 128 + description: Tags applied to every run this session triggers. + maxAttempts: + type: integer + minimum: 1 + maximum: 10 + description: Maximum retry attempts per run. + maxDuration: + type: integer + minimum: 1 + description: Per-run wall-clock cap in seconds. + lockToVersion: + type: string + description: Pin every run to a specific worker version. + example: "20240523.1" + region: + type: string + description: Region to schedule runs in. + idleTimeoutInSeconds: + type: integer + minimum: 1 + maximum: 3600 + description: Idle timeout surfaced to `chat.agent` via the wire payload. + CreateSessionRequestBody: + type: object + description: >- + Body for `POST /api/v1/sessions`. The whole body must be 32KB or smaller. + required: + - type + - taskIdentifier + - triggerConfig + properties: + type: + type: string + minLength: 1 + maxLength: 64 + description: >- + Free-form discriminator for the session, e.g. `chat.agent`. Not validated + against an enum. + example: chat.agent + taskIdentifier: + type: string + minLength: 1 + maxLength: 128 + description: The task this session triggers runs against. + example: my-chat + triggerConfig: + $ref: "#/components/schemas/SessionTriggerConfig" + externalId: + type: string + minLength: 1 + maxLength: 256 + description: >- + Your stable identity for the session, unique per environment. Cannot start + with the reserved `session_` prefix. Reusing an `externalId` makes create + idempotent; reusing one whose session is closed or expired returns `409`. + example: chat_1234 + tags: + type: array + maxItems: 10 + items: + type: string + maxLength: 128 + description: Up to 10 tags on the session row, for dashboard filtering. + metadata: + type: object + additionalProperties: true + description: Arbitrary JSON metadata. + expiresAt: + type: string + format: date-time + description: Absolute expiry timestamp for retention. + SessionObject: + type: object + description: A session row. + required: + - id + - type + - taskIdentifier + - tags + - createdAt + - updatedAt + properties: + id: + type: string + description: The session's friendly ID, prefixed with `session_`. + example: session_abc123 + externalId: + type: string + nullable: true + description: Your stable identity for the session, if one was set. + example: chat_1234 + type: + type: string + description: The session type discriminator. + example: chat.agent + taskIdentifier: + type: string + description: The task this session triggers runs against. + example: my-chat + triggerConfig: + $ref: "#/components/schemas/SessionTriggerConfig" + currentRunId: + type: string + nullable: true + description: >- + Friendly ID of the live run for this session, if any. Prefixed with `run_`. + Omitted on list rows. + example: run_def456 + tags: + type: array + items: + type: string + description: Tags on the session row. + example: ["chat:1234"] + metadata: + type: object + additionalProperties: true + nullable: true + description: Arbitrary JSON metadata, or `null` if unset. + closedAt: + type: string + format: date-time + nullable: true + description: When the session was closed, or `null` if open. + closedReason: + type: string + nullable: true + description: The optional reason recorded when the session was closed. + expiresAt: + type: string + format: date-time + nullable: true + description: The session's retention deadline, or `null` if none. + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + CreatedSessionResponseBody: + allOf: + - $ref: "#/components/schemas/SessionObject" + - type: object + required: + - runId + - publicAccessToken + - isCached + properties: + runId: + type: string + description: Friendly ID of the first run triggered alongside the session. + example: run_def456 + publicAccessToken: + type: string + description: >- + Session-scoped public access token carrying `read:sessions:{key}` and + `write:sessions:{key}`. Default TTL is 1 hour. Safe to pass to frontend + clients. + isCached: + type: boolean + description: >- + `true` if an open session already existed for this `externalId` + (idempotent upsert), `false` if newly created. + UpdateSessionRequestBody: + type: object + description: >- + Body for `PATCH /api/v1/sessions/{session}`. The whole body must be 32KB or + smaller. Every field is optional; omitted fields are left unchanged. + properties: + tags: + type: array + maxItems: 10 + items: + type: string + maxLength: 128 + description: Replaces the tags on the session row. + metadata: + type: object + additionalProperties: true + nullable: true + description: Replaces the metadata. Pass `null` to clear it. + externalId: + type: string + nullable: true + minLength: 1 + maxLength: 256 + description: >- + Read-only after create. Sending a value different from the current one + returns `422`; sending the same value is idempotent, and `null` clears it. + CloseSessionRequestBody: + type: object + description: Body for `POST /api/v1/sessions/{session}/close`. Up to 1KB. + properties: + reason: + type: string + maxLength: 256 + description: Optional reason recorded on the session row. + example: user signed out + SessionsFilter: + type: object + properties: + type: + type: array + items: + type: string + description: The session type(s) to filter by. + example: ["chat.agent"] + tags: + type: array + items: + type: string + description: The tags to filter by. + taskIdentifier: + type: array + items: + type: string + description: The task identifier(s) to filter by. + externalId: + type: string + description: Exact match on your `externalId`. + status: + type: array + items: + type: string + enum: + - ACTIVE + - CLOSED + - EXPIRED + description: The lifecycle status(es) to filter by. + createdAt: + type: object + properties: + from: + type: string + format: date-time + description: The start date to filter the sessions by. + to: + type: string + format: date-time + description: The end date to filter the sessions by. + period: + type: string + description: The period to filter the sessions by. + example: 1d + ListSessionsResult: + type: object + properties: + data: + type: array + items: + "$ref": "#/components/schemas/SessionObject" + pagination: + type: object + properties: + next: + type: string + description: >- + The session ID to start the next page after. Pass it as the + `page[after]` parameter on the next request. + example: session_abc123 + previous: + type: string + description: >- + The session ID to start the previous page before. Pass it as the + `page[before]` parameter on the next request. + example: session_xyz789 + InvalidEnvVarsRequestResponse: type: object properties: From b5b06ee72fb99738b6621d174619ce4a6f53169e Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 14 Jun 2026 18:35:38 +0100 Subject: [PATCH 2/2] docs: fix session externalId, list pagination, and read-scope docs From bot review of the Sessions API docs: - externalId is read-only after create (cannot be changed or cleared, it keys the durable streams and token scope). Drop the "null clears it" claim from the OpenAPI spec and the ai-chat/sessions page. - List sessions uses a sessions-specific pagination parameter (min 1, default 20) instead of the shared runs one (min 10, default 25, "runs per page"). - read:sessions grants listing the session's runs, not listing sessions. --- docs/ai-chat/sessions.mdx | 2 +- docs/management/authentication.mdx | 2 +- docs/v3-openapi.yaml | 36 +++++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/ai-chat/sessions.mdx b/docs/ai-chat/sessions.mdx index ba012891e2d..f003b06f7ed 100644 --- a/docs/ai-chat/sessions.mdx +++ b/docs/ai-chat/sessions.mdx @@ -138,7 +138,7 @@ console.log(session.currentRunId, session.tags, session.closedAt); ### `sessions.update(idOrExternalId, body, requestOptions?)` -Mutate `tags`, `metadata`, or `externalId` on an existing Session. Pass `externalId: null` to explicitly clear it. +Mutate `tags` or `metadata` on an existing Session. `externalId` is read-only after create: it cannot be changed or cleared (it keys the session's durable streams and token scope), so sending a different value returns `422`. ### `sessions.close(idOrExternalId, body?, requestOptions?)` diff --git a/docs/management/authentication.mdx b/docs/management/authentication.mdx index d1ac301b72f..b467fe531a5 100644 --- a/docs/management/authentication.mdx +++ b/docs/management/authentication.mdx @@ -196,7 +196,7 @@ Unlike `TriggerClient` instances (which stay isolated unless you opt in), `auth. | Scope | Grants | | --- | --- | -| `read:sessions:{id}` | Retrieve and list the session, and subscribe to and drain both its `.in` and `.out` [channels](/management/sessions/channels). | +| `read:sessions:{id}` | Retrieve the session, list its runs, and subscribe to and drain both its `.in` and `.out` [channels](/management/sessions/channels). | | `write:sessions:{id}` | Append to the session's `.in` channel, and create runs on the session (including the create call itself). | Two boundaries follow from the table, and both are enforced server-side: diff --git a/docs/v3-openapi.yaml b/docs/v3-openapi.yaml index 01ddb0fad0c..56100db3fe6 100644 --- a/docs/v3-openapi.yaml +++ b/docs/v3-openapi.yaml @@ -3350,7 +3350,7 @@ paths: List rows omit `triggerConfig`; retrieve a single session to read it. parameters: - - $ref: "#/components/parameters/cursorPagination" + - $ref: "#/components/parameters/sessionsCursorPagination" - $ref: "#/components/parameters/sessionsFilter" responses: "200": @@ -3430,8 +3430,9 @@ paths: Requires a secret key — a session public token cannot update a session. - `externalId` is read-only after create: sending a different value returns - `422`; sending the same value (or `null` to clear) is accepted. + `externalId` is read-only after create: it cannot be changed or cleared. + Sending a value different from the current one (including `null` when one is + set) returns `422`; sending the same value is accepted as a no-op. requestBody: required: true content: @@ -3576,6 +3577,30 @@ components: before: type: string description: The ID of the run to start the page before. This will set the direction of the pagination to backward. + sessionsCursorPagination: + in: query + name: page + style: deepObject + explode: true + description: | + Paginate the results. Specify the number of sessions per page, and the ID of the session to start the page after or before. + + For object fields like `page`, use the "form" encoding style. For example, to get the next page, use `page[after]=session_1234`. + schema: + type: object + properties: + size: + type: integer + maximum: 100 + minimum: 1 + default: 20 + description: Number of sessions per page. Maximum is 100. + after: + type: string + description: The ID of the session to start the page after. Sets the pagination direction to forward. + before: + type: string + description: The ID of the session to start the page before. Sets the pagination direction to backward. runId: in: path name: runId @@ -4393,8 +4418,9 @@ components: minLength: 1 maxLength: 256 description: >- - Read-only after create. Sending a value different from the current one - returns `422`; sending the same value is idempotent, and `null` clears it. + Read-only after create: cannot be changed or cleared. Sending a value + different from the current one (including `null` when one is set) returns + `422`; sending the same value is idempotent. CloseSessionRequestBody: type: object description: Body for `POST /api/v1/sessions/{session}/close`. Up to 1KB.