Skip to content

Commit 3f8bc26

Browse files
docs(ai): add AI Chat with useChat guide
Comprehensive guide covering: - Quick start with chatTask + TriggerChatTransport - Backend patterns: simple (return streamText), complex (pipeChat), and manual (task + ChatTaskPayload) - Frontend options: dynamic tokens, extra data, self-hosting - ChatTaskPayload reference - Added to Writing tasks navigation near Streams Co-authored-by: Eric Allam <eric@trigger.dev>
1 parent eb2ccc0 commit 3f8bc26

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"tags",
7575
"runs/metadata",
7676
"tasks/streams",
77+
"guides/ai-chat",
7778
"run-usage",
7879
"context",
7980
"runs/priority",

docs/guides/ai-chat.mdx

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
---
2+
title: "AI Chat with useChat"
3+
sidebarTitle: "AI Chat (useChat)"
4+
description: "Run AI SDK chat completions as durable Trigger.dev tasks with built-in realtime streaming."
5+
---
6+
7+
## Overview
8+
9+
The `@trigger.dev/sdk` provides a custom [ChatTransport](https://sdk.vercel.ai/docs/ai-sdk-ui/transport) 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.
10+
11+
**How it works:**
12+
1. The frontend sends messages via `useChat``TriggerChatTransport`
13+
2. The transport triggers a Trigger.dev task with the conversation as payload
14+
3. The task streams `UIMessageChunk` events back via Trigger.dev's realtime streams
15+
4. The AI SDK's `useChat` processes the stream natively — text, tool calls, reasoning, etc.
16+
17+
No custom API routes needed. Your chat backend is a Trigger.dev task.
18+
19+
<Note>
20+
Requires `@trigger.dev/sdk` version **4.4.0 or later** and the `ai` package **v5.0.0 or later**.
21+
</Note>
22+
23+
## Quick start
24+
25+
### 1. Define a chat task
26+
27+
Use `chatTask` from `@trigger.dev/sdk/ai` to define a task that handles chat messages. The payload is automatically typed as `ChatTaskPayload`.
28+
29+
If you return a `StreamTextResult` from `run`, it's **automatically piped** to the frontend.
30+
31+
```ts trigger/chat.ts
32+
import { chatTask } from "@trigger.dev/sdk/ai";
33+
import { streamText, convertToModelMessages } from "ai";
34+
import { openai } from "@ai-sdk/openai";
35+
36+
export const myChat = chatTask({
37+
id: "my-chat",
38+
run: async ({ messages }) => {
39+
// messages is UIMessage[] from the frontend
40+
return streamText({
41+
model: openai("gpt-4o"),
42+
messages: convertToModelMessages(messages),
43+
});
44+
// Returning a StreamTextResult auto-pipes it to the frontend
45+
},
46+
});
47+
```
48+
49+
### 2. Generate an access token
50+
51+
On your server (e.g. a Next.js API route or server action), create a trigger public token:
52+
53+
```ts app/actions.ts
54+
"use server";
55+
56+
import { auth } from "@trigger.dev/sdk";
57+
58+
export async function getChatToken() {
59+
return await auth.createTriggerPublicToken("my-chat");
60+
}
61+
```
62+
63+
### 3. Use in the frontend
64+
65+
Import `TriggerChatTransport` from `@trigger.dev/sdk/chat` (browser-safeno server dependencies).
66+
67+
```tsx app/components/chat.tsx
68+
"use client";
69+
70+
import { useChat } from "@ai-sdk/react";
71+
import { TriggerChatTransport } from "@trigger.dev/sdk/chat";
72+
73+
export function Chat({ accessToken }: { accessToken: string }) {
74+
const { messages, sendMessage, status, error } = useChat({
75+
transport: new TriggerChatTransport({
76+
task: "my-chat",
77+
accessToken,
78+
}),
79+
});
80+
81+
return (
82+
<div>
83+
{messages.map((m) => (
84+
<div key={m.id}>
85+
<strong>{m.role}:</strong>
86+
{m.parts.map((part, i) =>
87+
part.type === "text" ? <span key={i}>{part.text}</span> : null
88+
)}
89+
</div>
90+
))}
91+
92+
<form
93+
onSubmit={(e) => {
94+
e.preventDefault();
95+
const input = e.currentTarget.querySelector("input");
96+
if (input?.value) {
97+
sendMessage({ text: input.value });
98+
input.value = "";
99+
}
100+
}}
101+
>
102+
<input placeholder="Type a message..." />
103+
<button type="submit" disabled={status === "streaming"}>
104+
Send
105+
</button>
106+
</form>
107+
</div>
108+
);
109+
}
110+
```
111+
112+
## Backend patterns
113+
114+
### Simple: return a StreamTextResult
115+
116+
The easiest approach — return the `streamText` result from `run` and it's automatically piped to the frontend:
117+
118+
```ts
119+
import { chatTask } from "@trigger.dev/sdk/ai";
120+
import { streamText, convertToModelMessages } from "ai";
121+
import { openai } from "@ai-sdk/openai";
122+
123+
export const simpleChat = chatTask({
124+
id: "simple-chat",
125+
run: async ({ messages }) => {
126+
return streamText({
127+
model: openai("gpt-4o"),
128+
system: "You are a helpful assistant.",
129+
messages: convertToModelMessages(messages),
130+
});
131+
},
132+
});
133+
```
134+
135+
### Complex: use pipeChat() from anywhere
136+
137+
For complex agent flows where `streamText` is called deep inside your code, use `pipeChat()`. It works from **anywhere inside a task** — even nested function calls.
138+
139+
```ts trigger/agent-chat.ts
140+
import { chatTask, pipeChat } from "@trigger.dev/sdk/ai";
141+
import { streamText, convertToModelMessages } from "ai";
142+
import { openai } from "@ai-sdk/openai";
143+
144+
export const agentChat = chatTask({
145+
id: "agent-chat",
146+
run: async ({ messages }) => {
147+
// Don't return anything — pipeChat is called inside
148+
await runAgentLoop(convertToModelMessages(messages));
149+
},
150+
});
151+
152+
// This could be deep inside your agent library
153+
async function runAgentLoop(messages: CoreMessage[]) {
154+
// ... agent logic, tool calls, etc.
155+
156+
const result = streamText({
157+
model: openai("gpt-4o"),
158+
messages,
159+
});
160+
161+
// Pipe from anywhere — no need to return it
162+
await pipeChat(result);
163+
}
164+
```
165+
166+
### Manual: use task() with pipeChat()
167+
168+
If you need full control over task options, use the standard `task()` with `ChatTaskPayload` and `pipeChat()`:
169+
170+
```ts
171+
import { task } from "@trigger.dev/sdk";
172+
import { pipeChat, type ChatTaskPayload } from "@trigger.dev/sdk/ai";
173+
import { streamText, convertToModelMessages } from "ai";
174+
import { openai } from "@ai-sdk/openai";
175+
176+
export const manualChat = task({
177+
id: "manual-chat",
178+
retry: { maxAttempts: 3 },
179+
queue: { concurrencyLimit: 10 },
180+
run: async (payload: ChatTaskPayload) => {
181+
const result = streamText({
182+
model: openai("gpt-4o"),
183+
messages: convertToModelMessages(payload.messages),
184+
});
185+
186+
await pipeChat(result);
187+
},
188+
});
189+
```
190+
191+
## Frontend options
192+
193+
### TriggerChatTransport options
194+
195+
```ts
196+
new TriggerChatTransport({
197+
// Required
198+
task: "my-chat", // Task ID to trigger
199+
accessToken: token, // Trigger public token or secret key
200+
201+
// Optional
202+
baseURL: "https://...", // Custom API URL (self-hosted)
203+
streamKey: "chat", // Custom stream key (default: "chat")
204+
headers: { ... }, // Extra headers for API requests
205+
streamTimeoutSeconds: 120, // Stream timeout (default: 120s)
206+
});
207+
```
208+
209+
### Dynamic access tokens
210+
211+
For token refresh patterns, pass a function:
212+
213+
```ts
214+
new TriggerChatTransport({
215+
task: "my-chat",
216+
accessToken: () => getLatestToken(), // Called on each sendMessage
217+
});
218+
```
219+
220+
### Passing extra data
221+
222+
Use the `body` option on `sendMessage` to pass additional data to the task:
223+
224+
```ts
225+
sendMessage({
226+
text: "Hello",
227+
}, {
228+
body: {
229+
systemPrompt: "You are a pirate.",
230+
temperature: 0.9,
231+
},
232+
});
233+
```
234+
235+
The `body` fields are merged into the `ChatTaskPayload` and available in your task's `run` function.
236+
237+
## ChatTaskPayload
238+
239+
The payload sent to the task has this shape:
240+
241+
| Field | Type | Description |
242+
|-------|------|-------------|
243+
| `messages` | `UIMessage[]` | The conversation history |
244+
| `chatId` | `string` | Unique chat session ID |
245+
| `trigger` | `"submit-message" \| "regenerate-message"` | What triggered the request |
246+
| `messageId` | `string \| undefined` | Message ID to regenerate (if applicable) |
247+
| `metadata` | `unknown` | Custom metadata from the frontend |
248+
249+
Plus any extra fields from the `body` option.
250+
251+
## Self-hosting
252+
253+
If you're self-hosting Trigger.dev, pass the `baseURL` option:
254+
255+
```ts
256+
new TriggerChatTransport({
257+
task: "my-chat",
258+
accessToken,
259+
baseURL: "https://your-trigger-instance.com",
260+
});
261+
```
262+
263+
## Related
264+
265+
- [Realtime Streams](/tasks/streams) — How streams work under the hood
266+
- [Using the Vercel AI SDK](/guides/examples/vercel-ai-sdk) — Basic AI SDK usage with Trigger.dev
267+
- [Realtime React Hooks](/realtime/react-hooks/overview) — Lower-level realtime hooks
268+
- [Authentication](/realtime/auth) — Public access tokens and trigger tokens

0 commit comments

Comments
 (0)