Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/ai-chat/sessions.mdx
Comment thread
ericallam marked this conversation as resolved.
Comment thread
ericallam marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### `sessions.close(idOrExternalId, body?, requestOptions?)`

Expand Down Expand Up @@ -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?)`

Expand Down Expand Up @@ -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?)`

Expand Down Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
33 changes: 33 additions & 0 deletions docs/management/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</Note>

## 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 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). |
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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).
128 changes: 128 additions & 0 deletions docs/management/sessions/channels.mdx
Original file line number Diff line number Diff line change
@@ -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`.

<Warning>
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.
</Warning>

## 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": <record>, "id": <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.
4 changes: 4 additions & 0 deletions docs/management/sessions/close.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Close session"
openapi: "v3-openapi POST /api/v1/sessions/{session}/close"
---
4 changes: 4 additions & 0 deletions docs/management/sessions/create.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Create session"
openapi: "v3-openapi POST /api/v1/sessions"
---
Comment thread
coderabbitai[bot] marked this conversation as resolved.
4 changes: 4 additions & 0 deletions docs/management/sessions/list.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "List sessions"
openapi: "v3-openapi GET /api/v1/sessions"
---
4 changes: 4 additions & 0 deletions docs/management/sessions/retrieve.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Retrieve session"
openapi: "v3-openapi GET /api/v1/sessions/{session}"
---
4 changes: 4 additions & 0 deletions docs/management/sessions/update.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Update session"
openapi: "v3-openapi PATCH /api/v1/sessions/{session}"
---
Loading