Skip to main content
Trigger.dev is purpose-built for long-running background work, which makes it a natural fit for AI agents and LLM pipelines. Tasks can run for minutes or hours without timeouts, survive infrastructure interruptions, and pause mid-execution to wait for human input or external callbacks.

Why Trigger.dev for AI agents

AI workloads present challenges that typical HTTP request handlers cannot handle well:
  • Long run times — LLM chains, multi-step agents, and document processing pipelines can take minutes or hours. Trigger.dev tasks have no inherent timeout.
  • Durability — If a worker restarts mid-run, the task resumes where it left off rather than starting over.
  • Concurrency control — Use queues and concurrency limits to avoid overwhelming downstream APIs.
  • Retries — Transient failures (rate limits, network errors) are retried automatically with configurable backoff.
  • Human-in-the-loop — Pause a run indefinitely with wait.forToken() and resume it when a human approves or rejects.
  • Realtime streaming — Stream LLM output tokens back to your frontend as they are generated using streams.pipe().

AI patterns

LLM chain

Call one or more LLMs in sequence, passing the output of each step to the next. Trigger.dev handles retries and durability across each step.
import { task, retry } from "@trigger.dev/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

export const summarizeAndTranslate = task({
  id: "summarize-and-translate",
  retry: { maxAttempts: 3 },
  run: async (payload: { text: string; targetLanguage: string }) => {
    // Step 1: summarize
    const summary = await retry.onThrow(
      async () => {
        const res = await openai.chat.completions.create({
          model: "gpt-4o",
          messages: [
            { role: "system", content: "Summarize the following text in 3 sentences." },
            { role: "user", content: payload.text },
          ],
        });
        return res.choices[0].message.content ?? "";
      },
      { maxAttempts: 3 }
    );

    // Step 2: translate the summary
    const translated = await retry.onThrow(
      async () => {
        const res = await openai.chat.completions.create({
          model: "gpt-4o",
          messages: [
            {
              role: "system",
              content: `Translate the following text to ${payload.targetLanguage}.`,
            },
            { role: "user", content: summary },
          ],
        });
        return res.choices[0].message.content ?? "";
      },
      { maxAttempts: 3 }
    );

    return { summary, translated };
  },
});

Streaming LLM output

Pipe LLM output tokens to the Trigger.dev Realtime API so your frontend can display them as they arrive.
import { task, streams } from "@trigger.dev/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

export const streamingChat = task({
  id: "streaming-chat",
  run: async (payload: { prompt: string }) => {
    const completion = await openai.chat.completions.create({
      model: "gpt-4o",
      messages: [{ role: "user", content: payload.prompt }],
      stream: true,
    });

    // Pipe the stream to Realtime — subscribers receive chunks as they arrive
    const { stream, waitUntilComplete } = streams.pipe(completion);

    let fullText = "";
    for await (const chunk of stream) {
      fullText += chunk.choices[0]?.delta?.content ?? "";
    }

    await waitUntilComplete();

    return { text: fullText };
  },
});
On the frontend, use the useRealtimeRunWithStreams hook to receive the tokens:
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
import type { streamingChat } from "../trigger/streaming-chat";

export function ChatOutput({ runId, accessToken }: { runId: string; accessToken: string }) {
  const { run, streams } = useRealtimeRunWithStreams<
    typeof streamingChat,
    { default: OpenAI.Chat.ChatCompletionChunk }
  >(runId, { accessToken });

  const text = (streams.default ?? [])
    .map((chunk) => chunk.choices[0]?.delta?.content ?? "")
    .join("");

  return (
    <div>
      <p>{text}</p>
      {run?.status === "COMPLETED" && <span>Done</span>}
    </div>
  );
}

Agent loop

Build an agent that calls tools in a loop until it reaches a final answer. Long-running loops are safe in Trigger.dev tasks because there is no HTTP timeout.
import { task, metadata } from "@trigger.dev/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

const tools: OpenAI.Chat.ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "search_web",
      description: "Search the web for up-to-date information",
      parameters: {
        type: "object",
        properties: { query: { type: "string" } },
        required: ["query"],
      },
    },
  },
];

async function searchWeb(query: string): Promise<string> {
  // your search implementation
  return `Results for: ${query}`;
}

export const researchAgent = task({
  id: "research-agent",
  run: async (payload: { question: string }) => {
    const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
      { role: "user", content: payload.question },
    ];

    let iterations = 0;
    const maxIterations = 10;

    while (iterations < maxIterations) {
      iterations++;
      await metadata.set("iteration", iterations);

      const response = await openai.chat.completions.create({
        model: "gpt-4o",
        messages,
        tools,
        tool_choice: "auto",
      });

      const message = response.choices[0].message;
      messages.push(message);

      // No tool calls means the agent has reached a final answer
      if (!message.tool_calls || message.tool_calls.length === 0) {
        return { answer: message.content, iterations };
      }

      // Execute each tool call and append the results
      for (const toolCall of message.tool_calls) {
        if (toolCall.function.name === "search_web") {
          const args = JSON.parse(toolCall.function.arguments);
          const result = await searchWeb(args.query);
          messages.push({
            role: "tool",
            tool_call_id: toolCall.id,
            content: result,
          });
        }
      }
    }

    throw new Error(`Agent did not complete within ${maxIterations} iterations`);
  },
});

Human-in-the-loop

Pause an agent mid-run and wait for a human to approve or reject before continuing. The task suspends and releases its compute slot while waiting — you are not charged for idle time.
import { task, wait } from "@trigger.dev/sdk";

type ApprovalResult = { approved: boolean; comment?: string };

export const contentModerationTask = task({
  id: "content-moderation",
  run: async (payload: { content: string; submittedBy: string }) => {
    // Step 1: automated pre-screening
    const prescreen = await runAutoModeration(payload.content);

    if (prescreen.flagged) {
      // Step 2: create a token and pause until a human reviews it
      const token = await wait.createToken({
        timeout: "24h",
        tags: [`user:${payload.submittedBy}`, "moderation"],
      });

      // Send the token URL or ID to your moderation dashboard
      await notifyModerators({ content: payload.content, tokenId: token.id, tokenUrl: token.url });

      // Task suspends here until completeToken() is called
      const result = await wait.forToken<ApprovalResult>(token.id);

      if (!result.ok) {
        return { status: "timed_out" };
      }

      if (!result.output.approved) {
        return { status: "rejected", comment: result.output.comment };
      }
    }

    return { status: "approved" };
  },
});

async function runAutoModeration(content: string) {
  // Your moderation logic here (e.g., call an external AI moderation API)
  return { flagged: content.includes("spam") };
}

async function notifyModerators(opts: { content: string; tokenId: string; tokenUrl: string }) {
  // send an email, Slack message, etc.
}

Parallel sub-tasks

Fan out work across multiple child tasks and collect their results. Each child task runs independently with its own retry logic.
import { task } from "@trigger.dev/sdk";

export const analyzeDocuments = task({
  id: "analyze-documents",
  run: async (payload: { documentUrls: string[] }) => {
    // Trigger all analyses in parallel and wait for all results
    const results = await analyzeDocument.batchTriggerAndWait(
      payload.documentUrls.map((url) => ({ payload: { url } }))
    );

    const successful = results.runs.filter((r) => r.ok).map((r) => r.output);
    const failed = results.runs.filter((r) => !r.ok).length;

    return { successful, failed };
  },
});

export const analyzeDocument = task({
  id: "analyze-document",
  retry: { maxAttempts: 3 },
  run: async (payload: { url: string }) => {
    // fetch and analyze the document...
    return { url: payload.url, summary: "..." };
  },
});

Supported frameworks

Vercel AI SDK

Use streamText, generateText, and generateObject inside Trigger.dev tasks. Pipe the resulting stream to Realtime with streams.pipe().

OpenAI SDK

Works directly with openai.chat.completions.create({ stream: true }). The returned async iterable can be passed straight to streams.pipe().

Anthropic SDK

Use anthropic.messages.stream() inside a task and pipe the resulting stream to Trigger.dev Realtime for frontend consumption.

LangChain

Compose chains and agents with LangChain.js. Wrap each chain invocation in a Trigger.dev task for durability and automatic retries.

Next steps

Realtime streaming

Stream LLM output tokens to your frontend in real-time.

React hooks

Subscribe to runs and streams directly from React components.

Wait for token

Pause a task and resume it when a human approves or an external service responds.

MCP Server

Let your AI coding assistant interact directly with your Trigger.dev projects.

Build docs developers (and LLMs) love