Documentation Index
Fetch the complete documentation index at: https://trigger-docs-tri-7532-ai-sdk-chat-transport-and-chat-task-s.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Not released yet. This guide describes an upcoming release of
chat.agent
on top of the new Sessions primitive. The packages and server-side support
are still rolling out — we’ll remove this banner once the release ships and
publish the matching @trigger.dev/sdk prerelease tag.chat.agent during the prerelease period.
The public surface of chat.agent({...}), useTriggerChatTransport,
AgentChat, chat.store, chat.defer, and chat.history is largely
unchanged — but the transport’s auth callbacks and the server-side helpers
that feed them were reshaped, so most prerelease apps need a small wiring
update.
TL;DR
accessTokenis now a pure session-PAT mint — called only on 401/403 to refresh. It must return a token scoped to the session, not atrigger:tasksJWT.startSessionis a new callback that wraps a server action callingchat.createStartSessionAction(taskId). The transport invokes it ontransport.preload(chatId)and lazily on the firstsendMessagefor any chatId without a cached PAT.ChatSessionpersistable state dropsrunId— store only{publicAccessToken, lastEventId?}.- Per-call options on
transport.preload(chatId, ...)are gone. Trigger config (machine, idleTimeoutInSeconds, tags, queue, maxAttempts) lives server-side inchat.createStartSessionAction(taskId, options).
The architectural shift is that
chat.agent no longer rolls its own
per-run streams. It runs on top of a durable Session row that owns
its current run, persists across run lifecycles, and orchestrates
upgrades server-side. The customer-facing surface is similar; the wire
path beneath it changed completely.Step 1: Replace your access-token server action with two server actions
The old pattern was a single helper that minted a trigger token:app/actions.ts (before)
app/actions.ts (after)
chat.createStartSessionAction(taskId) returns a server action that:
- Creates the Session row for
chatId(idempotent on the(env, externalId)unique pair). - Triggers the agent task’s first run with
basePayload: {messages: [], trigger: "preload"}defaults plus any overrides you pass. - Returns
{sessionId, runId, publicAccessToken}to the browser.
Step 2: Update the transport wiring
The transport now takes two callbacks instead of one:app/components/chat.tsx (after)
| Trigger | Callback fired |
|---|---|
transport.preload(chatId) | startSession |
First sendMessage for a chatId with no cached PAT | startSession (auto) |
Any 401/403 from .in/append, .out SSE, or end-and-continue | accessToken |
Page hydrates with sessions: { [chatId]: ... } | Neither (uses hydrated PAT) |
startSession is deduped via an in-flight promise — concurrent
preload + sendMessage calls converge to one server action invocation.
Step 3: Drop transport-level trigger config
The prerelease transport acceptedtriggerConfig, triggerOptions, and
per-call options on preload. All of that moved server-side:
before
after
clientData on the transport (see
the next step) — it’s typed and threaded through startSession automatically.
Step 4: Use clientData for typed payload metadata
If your agent uses withClientData({schema}), the transport’s clientData
option is now the canonical place to set it. The same value:
- Is passed to your
startSessioncallback asparams.clientData, where you forward it intochat.createStartSessionAction’striggerConfig.basePayload.metadata. The agent’s first run sees it inpayload.metadata(visible toonPreload/onChatStart). - Merges into per-turn
metadataon every.in/appendchunk (visible toonTurnStart/ insiderunviaturn.clientData).
clientData value is live-updated when the option changes (the hook
calls setClientData under the hood), so dynamic values work without
reconstructing the transport.
Step 5: Update your ChatSession persistence
If you persist session state across page loads, drop the runId field:
before
after
runId column, you can drop it (the transport doesn’t
read it) or keep it for telemetry. The current run ID lives on the
Session row server-side now.
Hydration on page reload is unchanged:
chat.requestUpgrade(): same call, faster handoff
Calling chat.requestUpgrade() inside onTurnStart /
onValidateMessages still ends the current run so the next message starts
on the latest version. What changed is the mechanism:
- Before: the agent emitted a
trigger:upgrade-requiredchunk on.out; the transport consumed it browser-side and triggered a new run. - After: the agent calls
endAndContinueSessionserver-to-server; the webapp triggers a new run and atomically swapsSession.currentRunIdvia optimistic locking. The browser’s existing SSE subscription keeps receiving chunks across the swap — no transport-side bookkeeping.
SessionRun audit row with
reason: "upgrade" for dashboard provenance.
Hitting raw URLs
If your code talks to the realtime API directly instead of going through the SDK, the URL shapes changed:| Before | After |
|---|---|
GET /realtime/v1/streams/{runId}/chat | GET /realtime/v1/sessions/{chatId}/out |
POST /realtime/v1/streams/{runId}/{target}/chat-messages/append | POST /realtime/v1/sessions/{chatId}/in/append (body: {kind: "message", payload}) |
POST /realtime/v1/streams/{runId}/{target}/chat-stop/append | POST /realtime/v1/sessions/{chatId}/in/append (body: {kind: "stop"}) |
read:sessions:{chatId} + write:sessions:{chatId}) authorizes both the
externalId form (/sessions/my-chat-id/...) and the friendlyId form
(/sessions/session_abc.../...). The transport always uses the
externalId form; the friendlyId form is available for dashboard tooling
and direct API consumers.
What didn’t change
chat.agent({...})definition —id,idleTimeoutInSeconds,clientDataSchema,actionSchema,hydrateMessages,onPreload,onChatStart,onValidateMessages,onTurnStart,onTurnComplete,onChatSuspend,onAction,run. All callbacks have the same signature and fire at the same lifecycle points.chat.customAgent({...})and thechat.createSession(payload, ...)helper for building a session loop manually inside a custom agent.chat.store(snapshot store),chat.defer(deferred work), andchat.history(imperative history mutations from insideonAction).AgentChat(server-side chat client) —agent,id,clientData,session,onTriggered,onTurnComplete,sendMessage,text().useTriggerChatTransportReact semantics (created once, kept in a ref, callbacks updated under the hood).- Multi-tab coordination (
multiTab: true), pending messages / steering, background injection, compaction. - Per-turn
metadataflowing throughsendMessage({ text }, { metadata })toturn.metadataserver-side.
Verifying the migration
After updating, the smoke check is the same as before: send a message, confirm the assistant streams a response, reload mid-stream, confirm resume. A few new things worth verifying once you’ve cut over:- Eager preload. Click the button (or call
transport.preload(id)programmatically) — yourstartSessioncallback should fire and a Session row + first run should be created before you send a message. - Idle-timeout continuation. Wait past the agent’s
idleTimeoutInSecondsso the run exits, then send another message — the transport’s.in/appendshould boot a new run on the same Session, with aSessionRunrow ofreason: "continuation". - PAT refresh. Force a stale PAT in your DB (corrupt the signature)
and reload — the first request should 401, your
accessTokencallback should fire, and the retry should succeed.
- Your
accessTokencallback returns a token minted viaauth.createPublicToken({scopes: {sessions: chatId}}), notchat.createAccessTokenorauth.createTriggerPublicToken. The transport rejects trigger tokens now. - Your
startSessioncallback returns{publicAccessToken: string}— the result ofchat.createStartSessionAction(taskId)({chatId, ...})already has this shape. - You haven’t left a stale
getStartTokenoption on the transport; it’s not part ofTriggerChatTransportOptionsanymore.

