Overview
The @trigger.dev/sdk provides a custom ChatTransport for the Vercel AI SDK’s useChat hook. This lets you run chat completions as durable Trigger.dev tasks instead of fragile API routes — with automatic retries, observability, and realtime streaming built in.
How it works:
- The frontend sends messages via
useChat → TriggerChatTransport
- The transport triggers a Trigger.dev task with the conversation as payload
- The task streams
UIMessageChunk events back via Trigger.dev’s realtime streams
- The AI SDK’s
useChat processes the stream natively — text, tool calls, reasoning, etc.
No custom API routes needed. Your chat backend is a Trigger.dev task.
Requires @trigger.dev/sdk version 4.4.0 or later and the ai package v5.0.0 or later.
Quick start
1. Define a chat task
Use chat.task from @trigger.dev/sdk/ai to define a task that handles chat messages. The payload is automatically typed as ChatTaskPayload with abort signals.
If you return a StreamTextResult from run, it’s automatically piped to the frontend.
import { chat } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export const myChat = chat.task({
id: "my-chat",
run: async ({ messages, signal }) => {
// messages is UIMessage[] from the frontend
// signal fires on stop or run cancel
return streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(messages),
abortSignal: signal,
});
// Returning a StreamTextResult auto-pipes it to the frontend
},
});
2. Generate an access token
On your server (e.g. a Next.js API route or server action), create a trigger public token:
"use server";
import { auth } from "@trigger.dev/sdk";
export async function getChatToken() {
return await auth.createTriggerPublicToken("my-chat");
}
3. Use in the frontend
Import TriggerChatTransport from @trigger.dev/sdk/chat (browser-safe — no server dependencies).
"use client";
import { useChat } from "@ai-sdk/react";
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
export function Chat({ accessToken }: { accessToken: string }) {
const { messages, sendMessage, status, error } = useChat({
transport: new TriggerChatTransport({
task: "my-chat",
accessToken,
}),
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>
{m.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.querySelector("input");
if (input?.value) {
sendMessage({ text: input.value });
input.value = "";
}
}}
>
<input placeholder="Type a message..." />
<button type="submit" disabled={status === "streaming"}>
Send
</button>
</form>
</div>
);
}
Backend patterns
Simple: return a StreamTextResult
The easiest approach — return the streamText result from run and it’s automatically piped to the frontend:
import { chat } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export const simpleChat = chat.task({
id: "simple-chat",
run: async ({ messages, signal }) => {
return streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
messages: convertToModelMessages(messages),
abortSignal: signal,
});
},
});
Complex: use chat.pipe() from anywhere
For complex agent flows where streamText is called deep inside your code, use chat.pipe(). It works from anywhere inside a task — even nested function calls.
import { chat } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export const agentChat = chat.task({
id: "agent-chat",
run: async ({ messages }) => {
// Don't return anything — chat.pipe is called inside
await runAgentLoop(convertToModelMessages(messages));
},
});
// This could be deep inside your agent library
async function runAgentLoop(messages: CoreMessage[]) {
// ... agent logic, tool calls, etc.
const result = streamText({
model: openai("gpt-4o"),
messages,
});
// Pipe from anywhere — no need to return it
await chat.pipe(result);
}
Manual: use task() with chat.pipe()
If you need full control over task options, use the standard task() with ChatTaskPayload and chat.pipe():
import { task } from "@trigger.dev/sdk";
import { chat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
import { streamText, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
export const manualChat = task({
id: "manual-chat",
retry: { maxAttempts: 3 },
queue: { concurrencyLimit: 10 },
run: async (payload: ChatTaskPayload) => {
const result = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(payload.messages),
});
await chat.pipe(result);
},
});
Frontend options
TriggerChatTransport options
new TriggerChatTransport({
// Required
task: "my-chat", // Task ID to trigger
accessToken: token, // Trigger public token or secret key
// Optional
baseURL: "https://...", // Custom API URL (self-hosted)
streamKey: "chat", // Custom stream key (default: "chat")
headers: { ... }, // Extra headers for API requests
streamTimeoutSeconds: 120, // Stream timeout (default: 120s)
});
Dynamic access tokens
For token refresh patterns, pass a function:
new TriggerChatTransport({
task: "my-chat",
accessToken: () => getLatestToken(), // Called on each sendMessage
});
Use the body option on sendMessage to pass additional data to the task:
sendMessage({
text: "Hello",
}, {
body: {
systemPrompt: "You are a pirate.",
temperature: 0.9,
},
});
The body fields are merged into the ChatTaskPayload and available in your task’s run function.
ChatTaskPayload
The payload sent to the task has this shape:
| Field | Type | Description |
|---|
messages | UIMessage[] | The conversation history |
chatId | string | Unique chat session ID |
trigger | "submit-message" | "regenerate-message" | What triggered the request |
messageId | string | undefined | Message ID to regenerate (if applicable) |
metadata | unknown | Custom metadata from the frontend |
Plus any extra fields from the body option.
Self-hosting
If you’re self-hosting Trigger.dev, pass the baseURL option:
new TriggerChatTransport({
task: "my-chat",
accessToken,
baseURL: "https://your-trigger-instance.com",
});