diff --git a/docs/docs.json b/docs/docs.json
index 609ff7b3e16..44328ba3a89 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -472,6 +472,7 @@
},
"pages": [
"guides/ai-agents/overview",
+ "guides/ai-agents/chat-agent",
"guides/ai-agents/generate-translate-copy",
"guides/ai-agents/route-question",
"guides/ai-agents/respond-and-check-content",
diff --git a/docs/guides/ai-agents/chat-agent.mdx b/docs/guides/ai-agents/chat-agent.mdx
new file mode 100644
index 00000000000..f6465163a9b
--- /dev/null
+++ b/docs/guides/ai-agents/chat-agent.mdx
@@ -0,0 +1,124 @@
+---
+title: "Build a chat agent"
+sidebarTitle: "Chat agent"
+description: "Create a durable, multi-turn chat agent with chat.agent(), then add tools to it like any AI SDK agent."
+---
+
+## Overview
+
+Build a **durable, multi-turn chat agent**. A durable session owns the conversation, streams tokens to your UI, and stays alive across many back-and-forth messages. The other guides in this section are one-shot workflows (trigger a task, run a fixed sequence of LLM calls, return a result); a chat agent instead owns the session for its whole lifetime.
+
+[`chat.agent()`](/ai-chat/overview) handles the queuing, retries, resumability and streaming for you. You write the model call, Trigger.dev owns the session. For the full feature set (sessions, fast starts, compaction, sub-agents, the frontend transport), see the [AI chat docs](/ai-chat/overview).
+
+## A minimal agent
+
+Define an agent with `chat.agent()`. The `run` function receives the conversation `messages` (already converted from the frontend's `UIMessage[]`) and an abort `signal`. Return a `StreamTextResult` and it's piped to the frontend automatically.
+
+```typescript trigger/chat.ts
+import { chat } from "@trigger.dev/sdk/ai";
+import { anthropic } from "@ai-sdk/anthropic";
+import { streamText, stepCountIs } from "ai";
+
+export const myChat = chat.agent({
+ id: "my-chat",
+ run: async ({ messages, signal }) => {
+ return streamText({
+ // Spread chat.toStreamTextOptions() FIRST: it wires up prepareStep
+ // (compaction, steering, background injection) and telemetry.
+ ...chat.toStreamTextOptions(),
+ model: anthropic("claude-sonnet-4-5"),
+ messages,
+ abortSignal: signal,
+ stopWhen: stepCountIs(15),
+ });
+ },
+});
+```
+
+
+ Always spread `chat.toStreamTextOptions()` into your `streamText` call, and spread it first. It
+ wires up the `prepareStep` callback that drives compaction, mid-turn steering and background
+ injection. Those features silently no-op if the spread is missing.
+
+
+## Add tools
+
+A chat agent uses tools exactly like any other AI SDK agent. Declare them on the config so their results survive across turns, then pass the `tools` you receive in `run` straight to `streamText`:
+
+```typescript trigger/chat.ts
+import { chat } from "@trigger.dev/sdk/ai";
+import { anthropic } from "@ai-sdk/anthropic";
+import { streamText, stepCountIs, tool } from "ai";
+import { z } from "zod";
+
+const getCurrentTime = tool({
+ description: "Get the current server time as an ISO string.",
+ inputSchema: z.object({}),
+ execute: async () => ({ now: new Date().toISOString() }),
+});
+
+export const myChat = chat.agent({
+ id: "my-chat",
+ // Declared here so tool results survive history re-conversion across turns.
+ tools: { getCurrentTime },
+ run: async ({ messages, tools, signal }) => {
+ return streamText({
+ // Pass tools INTO toStreamTextOptions (not separately to streamText):
+ // this is what detects tool calls needing HITL approval and merges any
+ // auto-injected skill tools. It sets streamText's `tools` for you.
+ ...chat.toStreamTextOptions({ tools }),
+ model: anthropic("claude-sonnet-4-5"),
+ messages,
+ stopWhen: stepCountIs(15),
+ abortSignal: signal,
+ });
+ },
+});
+```
+
+Swap `getCurrentTime` for whatever your agent needs to do: query a database, call an API, or trigger another Trigger.dev task. See [Tools](/ai-chat/tools) for how tool results are persisted and replayed across turns.
+
+## Wire up the frontend
+
+The browser talks to Trigger.dev directly through the [chat transport](/ai-chat/frontend), so there's no API route to maintain. Expose two server actions (one to start the session, one to mint a session-scoped token) and pass them to `useTriggerChatTransport`, then hand the transport to the AI SDK's `useChat`:
+
+```typescript app/actions.ts
+"use server";
+
+import { auth } from "@trigger.dev/sdk";
+import { chat } from "@trigger.dev/sdk/ai";
+
+export const startChatSession = chat.createStartSessionAction("my-chat");
+
+export async function mintChatAccessToken(chatId: string) {
+ // Authorize the caller for this chatId before minting: confirm the logged-in
+ // user owns this session (e.g. look it up in your database). Otherwise anyone
+ // who learns a session ID could mint read/write access to it.
+ return auth.createPublicToken({
+ scopes: { read: { sessions: chatId }, write: { sessions: chatId } },
+ expirationTime: "1h",
+ });
+}
+```
+
+See the [Quick Start](/ai-chat/quick-start) for the complete frontend component.
+
+## A full example
+
+For a complete, real-world chat agent, see the ClickHouse chat agent example. It builds on everything above with generative UI, a versioned system prompt, and real tools against a live database.
+
+
+
+ A full example project: a chat agent that answers questions about your data with charts, tables
+ and maps.
+
+
+ How chat agents, sessions and the turn loop work.
+
+
+ Declaring tools on your agent and how they persist across turns.
+
+
+ Cut first-turn latency with preload and head start.
+
+
diff --git a/docs/guides/ai-agents/generate-translate-copy.mdx b/docs/guides/ai-agents/generate-translate-copy.mdx
index cb5e034aa31..1c16a05a3cb 100644
--- a/docs/guides/ai-agents/generate-translate-copy.mdx
+++ b/docs/guides/ai-agents/generate-translate-copy.mdx
@@ -16,14 +16,14 @@ In this example, we'll create a workflow that generates and translates copy. Thi
**This task:**
-- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
-- Uses `experimental_telemetry` to provide LLM logs
+- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models
+- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Generates marketing copy based on subject and target word count
- Validates the generated copy meets word count requirements (±10 words)
- Translates the validated copy to the target language while preserving tone
```typescript
-import { openai } from "@ai-sdk/openai";
+import { anthropic } from "@ai-sdk/anthropic";
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
@@ -39,7 +39,7 @@ export const generateAndTranslateTask = task({
run: async (payload: TranslatePayload) => {
// Step 1: Generate marketing copy
const generatedCopy = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -72,7 +72,7 @@ export const generateAndTranslateTask = task({
// Step 2: Translate to target language
const translatedCopy = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -103,9 +103,9 @@ On the Test page in the dashboard, select the `generate-and-translate-copy` task
```json
{
- marketingSubject: "The controversial new Jaguar electric concept car",
- targetLanguage: "Spanish",
- targetWordCount: 100,
+ "marketingSubject": "The controversial new Jaguar electric concept car",
+ "targetLanguage": "Spanish",
+ "targetWordCount": 100
}
```
diff --git a/docs/guides/ai-agents/overview.mdx b/docs/guides/ai-agents/overview.mdx
index 0c27982a450..fd1d5c0a572 100644
--- a/docs/guides/ai-agents/overview.mdx
+++ b/docs/guides/ai-agents/overview.mdx
@@ -22,6 +22,14 @@ description: "Real world AI agent example tasks using Trigger.dev"
Generate and maintain GitHub wiki documentation with Claude-powered analysis.
+
+ Build a chat agent that answers questions about your ClickHouse data with charts, tables and maps
+ using `chat.agent()` and generative UI.
+
+## Chat agents
+
+Build a durable, multi-turn chat agent with [`chat.agent()`](/ai-chat/overview). A durable session per conversation, with streaming and resumability handled for you.
+
+
+
+ Create a durable, multi-turn chat agent with `chat.agent()`, then add tools to it.
+
+
+
## Agent fundamentals
These guides will show you how to set up different types of AI agent workflows with Trigger.dev. The examples take inspiration from Anthropic's blog post on [building effective agents](https://www.anthropic.com/research/building-effective-agents).
diff --git a/docs/guides/ai-agents/respond-and-check-content.mdx b/docs/guides/ai-agents/respond-and-check-content.mdx
index 560b69b80c5..116216c673f 100644
--- a/docs/guides/ai-agents/respond-and-check-content.mdx
+++ b/docs/guides/ai-agents/respond-and-check-content.mdx
@@ -15,14 +15,14 @@ In this example, we'll create a workflow that simultaneously checks content for
**This task:**
-- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
-- Uses `experimental_telemetry` to provide LLM logs
+- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models
+- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to run customer response and content moderation tasks in parallel
-- Generates customer service responses using an AI model
+- Answers with `claude-sonnet-4-5` and moderates with the faster, cheaper `claude-haiku-4-5`
- Simultaneously checks for inappropriate content while generating responses
```typescript
-import { openai } from "@ai-sdk/openai";
+import { anthropic } from "@ai-sdk/anthropic";
import { batch, task } from "@trigger.dev/sdk";
import { generateText } from "ai";
@@ -31,7 +31,7 @@ export const generateCustomerResponse = task({
id: "generate-customer-response",
run: async (payload: { question: string }) => {
const response = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -54,7 +54,7 @@ export const checkInappropriateContent = task({
id: "check-inappropriate-content",
run: async (payload: { text: string }) => {
const response = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-haiku-4-5"),
messages: [
{
role: "system",
diff --git a/docs/guides/ai-agents/route-question.mdx b/docs/guides/ai-agents/route-question.mdx
index de1cb3e92fd..717b82cbbca 100644
--- a/docs/guides/ai-agents/route-question.mdx
+++ b/docs/guides/ai-agents/route-question.mdx
@@ -16,78 +16,57 @@ In this example, we'll create a workflow that routes a question to a different A
**This task:**
-- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
-- Uses `experimental_telemetry` in the source verification and historical analysis tasks to provide LLM logs
-- Routes questions using a lightweight model (`o1-mini`) to classify complexity
-- Directs simple questions to `gpt-4o` and complex ones to `gpt-o3-mini`
+- Uses `generateObject` from the [AI SDK](https://ai-sdk.dev/) to classify the question into a typed routing decision
+- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
+- Classifies complexity with a fast, cheap model (`claude-haiku-4-5`)
+- Directs simple questions to `claude-haiku-4-5` and complex ones to `claude-sonnet-4-5`
- Returns both the answer and metadata about the routing decision
```typescript
-import { openai } from "@ai-sdk/openai";
+import { anthropic } from "@ai-sdk/anthropic";
import { task } from "@trigger.dev/sdk";
-import { generateText } from "ai";
+import { generateObject, generateText } from "ai";
import { z } from "zod";
-// Schema for router response
+// The router's structured decision. generateObject validates the model
+// output against this schema, so there's no manual JSON parsing.
const routingSchema = z.object({
- model: z.enum(["gpt-4o", "gpt-o3-mini"]),
+ model: z.enum(["claude-haiku-4-5", "claude-sonnet-4-5"]),
reason: z.string(),
});
-// Router prompt template
-const ROUTER_PROMPT = `You are a routing assistant that determines the complexity of questions.
-Analyze the following question and route it to the appropriate model:
-
-- Use "gpt-4o" for simple, common, or straightforward questions
-- Use "gpt-o3-mini" for complex, unusual, or questions requiring deep reasoning
-
-Respond with a JSON object in this exact format:
-{"model": "gpt-4o" or "gpt-o3-mini", "reason": "your reasoning here"}
-
-Question: `;
-
export const routeAndAnswerQuestion = task({
id: "route-and-answer-question",
run: async (payload: { question: string }) => {
- // Step 1: Route the question
- const routingResponse = await generateText({
- model: openai("o1-mini"),
- messages: [
- {
- role: "system",
- content:
- "You must respond with a valid JSON object containing only 'model' and 'reason' fields. No markdown, no backticks, no explanation.",
- },
- {
- role: "user",
- content: ROUTER_PROMPT + payload.question,
- },
- ],
- temperature: 0.1,
+ // Step 1: Classify the question and pick a model
+ const { object: routing } = await generateObject({
+ model: anthropic("claude-haiku-4-5"),
+ schema: routingSchema,
+ system:
+ "You are a routing assistant. Pick the model best suited to answer the question:\n" +
+ "- claude-haiku-4-5 for simple, common, or straightforward questions\n" +
+ "- claude-sonnet-4-5 for complex, unusual, or questions needing deep reasoning",
+ prompt: payload.question,
experimental_telemetry: {
isEnabled: true,
- functionId: "route-and-answer-question",
+ functionId: "route-question",
},
});
- // Add error handling and cleanup
- let jsonText = routingResponse.text.trim();
- if (jsonText.startsWith("```")) {
- jsonText = jsonText.replace(/```json\n|\n```/g, "");
- }
-
- const routingResult = routingSchema.parse(JSON.parse(jsonText));
-
- // Step 2: Get the answer using the selected model
- const answerResult = await generateText({
- model: openai(routingResult.model),
- messages: [{ role: "user", content: payload.question }],
+ // Step 2: Answer with the selected model
+ const answer = await generateText({
+ model: anthropic(routing.model),
+ prompt: payload.question,
+ experimental_telemetry: {
+ isEnabled: true,
+ functionId: "answer-question",
+ },
});
return {
- answer: answerResult.text,
- selectedModel: routingResult.model,
- routingReason: routingResult.reason,
+ answer: answer.text,
+ selectedModel: routing.model,
+ routingReason: routing.reason,
};
},
});
@@ -97,7 +76,7 @@ export const routeAndAnswerQuestion = task({
## Run a test
-Triggering our task with a simple question shows it routing to the gpt-4o model and returning the answer with reasoning:
+Triggering our task with a simple question shows it routing to the `claude-haiku-4-5` model and returning the answer with reasoning:
```json
{
diff --git a/docs/guides/ai-agents/translate-and-refine.mdx b/docs/guides/ai-agents/translate-and-refine.mdx
index 80f557fd0c3..f5958524c55 100644
--- a/docs/guides/ai-agents/translate-and-refine.mdx
+++ b/docs/guides/ai-agents/translate-and-refine.mdx
@@ -25,7 +25,7 @@ This example task translates text into a target language and refines the transla
```typescript
import { task } from "@trigger.dev/sdk";
import { generateText } from "ai";
-import { openai } from "@ai-sdk/openai";
+import { anthropic } from "@ai-sdk/anthropic";
interface TranslationPayload {
text: string;
@@ -35,9 +35,17 @@ interface TranslationPayload {
rejectionCount?: number;
}
+interface TranslationResult {
+ finalTranslation: string | undefined;
+ iterations: number;
+ status: "MAX_ITERATIONS_REACHED" | "APPROVED";
+}
+
export const translateAndRefine = task({
id: "translate-and-refine",
- run: async (payload: TranslationPayload) => {
+ // Explicit return type: the task returns its own recursive result, so
+ // annotate it to avoid TypeScript's circular-inference error.
+ run: async (payload: TranslationPayload): Promise => {
const rejectionCount = payload.rejectionCount || 0;
// Bail out if we've hit the maximum attempts
@@ -55,7 +63,7 @@ export const translateAndRefine = task({
: `Translate this text into ${payload.targetLanguage}, preserving style and meaning: "${payload.text}"`;
const translation = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -75,7 +83,7 @@ export const translateAndRefine = task({
// Evaluate the translation
const evaluation = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -83,7 +91,7 @@ export const translateAndRefine = task({
Your goal is to ensure translations are accurate and natural, but not necessarily perfect.
This is iteration ${
rejectionCount + 1
- } of a maximum 5 iterations.
+ } of a maximum 10 iterations.
RESPONSE FORMAT:
- If the translation meets 90%+ quality: Respond with exactly "APPROVED" (nothing else)
@@ -134,8 +142,9 @@ export const translateAndRefine = task({
};
}
- // If not approved, recursively call the task with feedback
- await translateAndRefine
+ // If not approved, recursively refine with feedback and return the
+ // refined result so the final translation propagates back up.
+ return await translateAndRefine
.triggerAndWait({
text: payload.text,
targetLanguage: payload.targetLanguage,
diff --git a/docs/guides/ai-agents/verify-news-article.mdx b/docs/guides/ai-agents/verify-news-article.mdx
index e235ae9d35c..e6bfe076c36 100644
--- a/docs/guides/ai-agents/verify-news-article.mdx
+++ b/docs/guides/ai-agents/verify-news-article.mdx
@@ -16,18 +16,18 @@ Our example task uses multiple LLM calls to extract claims from a news article a
**This task:**
-- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models
-- Uses `experimental_telemetry` to provide LLM logs
+- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models
+- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard
- Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to orchestrate parallel processing of claims
-- Extracts factual claims from news articles using the `o1-mini` model
+- Extracts factual claims from news articles using `claude-sonnet-4-5`
- Evaluates claims against recent sources and analyzes historical context in parallel
- Combines results into a structured analysis report
```typescript
-import { openai } from "@ai-sdk/openai";
+import { anthropic } from "@ai-sdk/anthropic";
import { batch, logger, task } from "@trigger.dev/sdk";
-import { CoreMessage, generateText } from "ai";
+import { ModelMessage, generateText } from "ai";
// Define types for our workers' outputs
interface Claim {
@@ -53,7 +53,7 @@ export const extractClaims = task({
id: "extract-claims",
run: async ({ article }: { article: string }) => {
try {
- const messages: CoreMessage[] = [
+ const messages: ModelMessage[] = [
{
role: "system",
content:
@@ -66,8 +66,12 @@ export const extractClaims = task({
];
const response = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages,
+ experimental_telemetry: {
+ isEnabled: true,
+ functionId: "extract-claims",
+ },
});
const claims = response.text
@@ -94,7 +98,7 @@ export const verifySource = task({
id: "verify-source",
run: async (claim: Claim) => {
const response = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
@@ -126,7 +130,7 @@ export const analyzeHistory = task({
id: "analyze-history",
run: async (claim: Claim) => {
const response = await generateText({
- model: openai("o1-mini"),
+ model: anthropic("claude-sonnet-4-5"),
messages: [
{
role: "system",
diff --git a/docs/guides/introduction.mdx b/docs/guides/introduction.mdx
index 116c8539b0d..aebad856f64 100644
--- a/docs/guides/introduction.mdx
+++ b/docs/guides/introduction.mdx
@@ -23,6 +23,7 @@ Get set up fast using our detailed walk-through guides.
| Guide | Description |
| :----------------------------------------------------------------------------------------- | :------------------------------------------------------------------- |
+| [AI Agent: Chat agent](/guides/ai-agents/chat-agent) | Build a durable, multi-turn chat agent with `chat.agent()` |
| [AI Agent: Content moderation](/guides/ai-agents/respond-and-check-content) | Parallel check content while responding to customers |
| [AI Agent: Generate and translate copy](/guides/ai-agents/generate-translate-copy) | Chain prompts to generate and translate content |
| [AI Agent: News verification](/guides/ai-agents/verify-news-article) | Orchestrate fact checking of news articles |
@@ -56,6 +57,7 @@ Example projects are full projects with example repos you can fork and use. Thes
| [Claude changelog generator](/guides/example-projects/claude-changelog-generator) | Automatically generate professional changelogs from git commits using Claude. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/changelog-generator) |
| [Claude GitHub wiki agent](/guides/example-projects/claude-github-wiki) | Generate and maintain GitHub wiki documentation with Claude-powered analysis. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-agent-github-wiki) |
| [Claude thinking chatbot](/guides/example-projects/claude-thinking-chatbot) | Use Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot) |
+| [ClickHouse chat agent](/guides/example-projects/clickhouse-chat-agent) | Build a chat agent that answers questions about your ClickHouse data with charts, tables and maps using `chat.agent()` and generative UI. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/clickhouse-chat-agent) |
| [Cursor background agent](/guides/example-projects/cursor-background-agent) | Run Cursor's headless CLI agent as a background task, streaming live output to the browser. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/cursor-cli-demo) |
| [Human-in-the-loop workflow](/guides/example-projects/human-in-the-loop-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow) |
| [Mastra agents with memory](/guides/example-projects/mastra-agents-with-memory) | Use Mastra to create a weather agent that can collect live weather data and generate clothing recommendations. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/mastra-agents) |