Skip to main content

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:
  1. The frontend sends messages via useChatTriggerChatTransport
  2. The transport triggers a Trigger.dev task with the conversation as payload
  3. The task streams UIMessageChunk events back via Trigger.dev’s realtime streams
  4. 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.
trigger/chat.ts
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:
app/actions.ts
"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).
app/components/chat.tsx
"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.
trigger/agent-chat.ts
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
});

Passing extra data

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:
FieldTypeDescription
messagesUIMessage[]The conversation history
chatIdstringUnique chat session ID
trigger"submit-message" | "regenerate-message"What triggered the request
messageIdstring | undefinedMessage ID to regenerate (if applicable)
metadataunknownCustom 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",
});