Skip to main content

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.
This guide is for customers who tried 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

// Single accessToken callback, dispatches on purpose
accessToken: async ({ chatId, purpose }) => {
  if (purpose === "trigger") {
    return chat.createAccessToken<typeof myChat>("my-chat");
  }
  // purpose === "preload" — same call, same trigger token
  return chat.createAccessToken<typeof myChat>("my-chat");
};
What changed:
  • accessToken is now a pure session-PAT mint — called only on 401/403 to refresh. It must return a token scoped to the session, not a trigger:tasks JWT.
  • startSession is a new callback that wraps a server action calling chat.createStartSessionAction(taskId). The transport invokes it on transport.preload(chatId) and lazily on the first sendMessage for any chatId without a cached PAT.
  • ChatSession persistable state drops runId — store only {publicAccessToken, lastEventId?}.
  • Per-call options on transport.preload(chatId, ...) are gone. Trigger config (machine, idleTimeoutInSeconds, tags, queue, maxAttempts) lives server-side in chat.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)
"use server";

import { chat } from "@trigger.dev/sdk/ai";
import type { myChat } from "@/trigger/chat";

export const getChatToken = () =>
  chat.createAccessToken<typeof myChat>("my-chat");
Replace with two helpers — one for session creation, one for PAT refresh:
app/actions.ts (after)
"use server";

import { auth } from "@trigger.dev/sdk";
import { chat } from "@trigger.dev/sdk/ai";

// Server-side wrapper for session creation. Idempotent on (env, chatId).
// The customer's server is the only entry point that creates Session rows;
// the browser never holds a `trigger:tasks` JWT.
export const startChatSession = chat.createStartSessionAction("my-chat");

// Pure session-PAT mint for the transport's 401/403 retry path.
export async function mintChatAccessToken(chatId: string) {
  return auth.createPublicToken({
    scopes: {
      read: { sessions: chatId },
      write: { sessions: chatId },
    },
    expirationTime: "1h",
  });
}
chat.createStartSessionAction(taskId) returns a server action that:
  1. Creates the Session row for chatId (idempotent on the (env, externalId) unique pair).
  2. Triggers the agent task’s first run with basePayload: {messages: [], trigger: "preload"} defaults plus any overrides you pass.
  3. 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)
"use client";

import { useChat } from "@ai-sdk/react";
import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react";
import type { myChat } from "@/trigger/chat";
import { mintChatAccessToken, startChatSession } from "@/app/actions";

export function Chat() {
  const transport = useTriggerChatTransport<typeof myChat>({
    task: "my-chat",
    accessToken: ({ chatId }) => mintChatAccessToken(chatId),
    startSession: ({ chatId, taskId, clientData }) =>
      startChatSession({ chatId, taskId, clientData }),
  });

  const { messages, sendMessage, status } = useChat({ transport });
  // ...
}
The transport calls them in two distinct flows:
TriggerCallback fired
transport.preload(chatId)startSession
First sendMessage for a chatId with no cached PATstartSession (auto)
Any 401/403 from .in/append, .out SSE, or end-and-continueaccessToken
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 accepted triggerConfig, triggerOptions, and per-call options on preload. All of that moved server-side:
before
const transport = useTriggerChatTransport({
  task: "my-chat",
  accessToken: getChatToken,
  triggerConfig: { basePayload: { /* ... */ } },
  triggerOptions: { tags: [...], machine: "small-1x", maxAttempts: 3 },
});

transport.preload(chatId, { idleTimeoutInSeconds: 60, metadata: { ... } });
after
// Trigger config now lives in chat.createStartSessionAction
export const startChatSession = chat.createStartSessionAction("my-chat", {
  triggerConfig: {
    machine: "small-1x",
    maxAttempts: 3,
    tags: ["my-tag"],
    idleTimeoutInSeconds: 60,
  },
});

// Browser side
const transport = useTriggerChatTransport<typeof myChat>({
  task: "my-chat",
  accessToken: ({ chatId }) => mintChatAccessToken(chatId),
  startSession: ({ chatId, taskId, clientData }) =>
    startChatSession({ chatId, taskId, clientData }),
});

transport.preload(chatId);  // no second arg
For metadata that varies per chat, use 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 startSession callback as params.clientData, where you forward it into chat.createStartSessionAction’s triggerConfig.basePayload.metadata. The agent’s first run sees it in payload.metadata (visible to onPreload / onChatStart).
  • Merges into per-turn metadata on every .in/append chunk (visible to onTurnStart / inside run via turn.clientData).
const transport = useTriggerChatTransport<typeof myChat>({
  task: "my-chat",
  accessToken: ({ chatId }) => mintChatAccessToken(chatId),
  startSession: ({ chatId, taskId, clientData }) =>
    startChatSession({ chatId, taskId, clientData }),
  clientData: {
    userId: currentUser.id,
    plan: currentUser.plan,
  },
});
The clientData value is live-updated when the option changes (the hook calls setClientData under the hood), so dynamic values work without reconstructing the transport.
Server-side authorization can still override or augment the browser-claimed clientData inside startSession — never trust the browser’s identity claim. A typical pattern: the server action looks up the user from the request session, then merges the trusted server fields on top of params.clientData.

Step 5: Update your ChatSession persistence

If you persist session state across page loads, drop the runId field:
before
type ChatSession = {
  runId: string;
  publicAccessToken: string;
  lastEventId?: string;
};
after
type ChatSession = {
  publicAccessToken: string;
  lastEventId?: string;
};
If your DB has a 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:
const transport = useTriggerChatTransport<typeof myChat>({
  // ...
  sessions: persistedSession
    ? { [chatId]: persistedSession }
    : {},
});

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-required chunk on .out; the transport consumed it browser-side and triggered a new run.
  • After: the agent calls endAndContinueSession server-to-server; the webapp triggers a new run and atomically swaps Session.currentRunId via optimistic locking. The browser’s existing SSE subscription keeps receiving chunks across the swap — no transport-side bookkeeping.
The new run is recorded in a 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:
BeforeAfter
GET /realtime/v1/streams/{runId}/chatGET /realtime/v1/sessions/{chatId}/out
POST /realtime/v1/streams/{runId}/{target}/chat-messages/appendPOST /realtime/v1/sessions/{chatId}/in/append (body: {kind: "message", payload})
POST /realtime/v1/streams/{runId}/{target}/chat-stop/appendPOST /realtime/v1/sessions/{chatId}/in/append (body: {kind: "stop"})
The session-scoped PAT (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 the chat.createSession(payload, ...) helper for building a session loop manually inside a custom agent.
  • chat.store (snapshot store), chat.defer (deferred work), and chat.history (imperative history mutations from inside onAction).
  • AgentChat (server-side chat client) — agent, id, clientData, session, onTriggered, onTurnComplete, sendMessage, text().
  • useTriggerChatTransport React 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 metadata flowing through sendMessage({ text }, { metadata }) to turn.metadata server-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) — your startSession callback should fire and a Session row + first run should be created before you send a message.
  • Idle-timeout continuation. Wait past the agent’s idleTimeoutInSeconds so the run exits, then send another message — the transport’s .in/append should boot a new run on the same Session, with a SessionRun row of reason: "continuation".
  • PAT refresh. Force a stale PAT in your DB (corrupt the signature) and reload — the first request should 401, your accessToken callback should fire, and the retry should succeed.
If any of those misfire, check that:
  • Your accessToken callback returns a token minted via auth.createPublicToken({scopes: {sessions: chatId}}), not chat.createAccessToken or auth.createTriggerPublicToken. The transport rejects trigger tokens now.
  • Your startSession callback returns {publicAccessToken: string} — the result of chat.createStartSessionAction(taskId)({chatId, ...}) already has this shape.
  • You haven’t left a stale getStartToken option on the transport; it’s not part of TriggerChatTransportOptions anymore.

Reference