Skip to main content
Trigger.dev Realtime lets you subscribe to background runs from your frontend or backend and receive live updates as they execute. It is built on top of Server-Sent Events (SSE) and works with any framework that supports streaming responses.

What you can subscribe to

ScopeDescription
Single runMonitor a specific run by its ID
Runs by tagTrack all runs that share a tag (e.g. user:123)
Batch runsAll runs within a batch
Trigger + subscribeTrigger a task and immediately subscribe to the new run (React hooks only)
Each subscription delivers the complete run object with automatic updates whenever the run changes.

What updates you receive

  • Status changesQUEUEDEXECUTINGCOMPLETED (or FAILED, CANCELED, etc.)
  • Metadata updates — Custom progress data written by your task using metadata.set()
  • Tag changes — When tags are added or removed from the run
  • Realtime streams — Binary or JSON chunks appended with streams.pipe() or streams.append() inside your task

Streaming AI output

Trigger.dev Realtime is designed to work seamlessly with LLM output streaming. Your task pipes tokens directly to the Realtime API, and subscribers receive them as they arrive.

In your task

Use streams.pipe() to forward an async iterable or ReadableStream to the Realtime API:
import { task, streams } from "@trigger.dev/sdk";
import OpenAI from "openai";

const openai = new OpenAI();

export const chatTask = task({
  id: "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 OpenAI stream to Realtime subscribers
    const { stream, waitUntilComplete } = streams.pipe(completion);

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

    // Ensure all chunks have been sent before the task completes
    await waitUntilComplete();

    return { text: fullText };
  },
});
streams.pipe() returns the original stream so you can consume it locally in the task while simultaneously forwarding chunks to subscribers. Call waitUntilComplete() before returning to guarantee all chunks have been flushed.

Multiple named streams

You can maintain multiple independent streams in a single run by passing a key:
import { task, streams } from "@trigger.dev/sdk";

export const multiStreamTask = task({
  id: "multi-stream",
  run: async (payload: { prompt: string }) => {
    // Reasoning stream
    const reasoningStream = await generateReasoning(payload.prompt);
    const { waitUntilComplete: waitReasoning } = streams.pipe("reasoning", reasoningStream);

    // Final answer stream
    const answerStream = await generateAnswer(payload.prompt);
    const { waitUntilComplete: waitAnswer } = streams.pipe("answer", answerStream);

    await waitReasoning();
    await waitAnswer();
  },
});

async function generateReasoning(prompt: string): Promise<AsyncIterable<string>> {
  // returns an async iterable of reasoning tokens
  return (async function* () { yield "thinking..."; })();
}

async function generateAnswer(prompt: string): Promise<AsyncIterable<string>> {
  return (async function* () { yield "answer here"; })();
}

Writing chunks manually

If you want to push individual values rather than pipe an existing stream, use streams.append():
import { task, streams } from "@trigger.dev/sdk";

export const progressTask = task({
  id: "progress-task",
  run: async (payload: { items: string[] }) => {
    for (let i = 0; i < payload.items.length; i++) {
      await processItem(payload.items[i]);
      await streams.append("progress", JSON.stringify({ done: i + 1, total: payload.items.length }));
    }
  },
});

async function processItem(item: string) {
  // process the item
}

Authentication

All Realtime subscriptions require a Public Access Token. These are short-lived JWT tokens that are safe to expose to the browser. There are two ways to obtain one:
  1. From the run handle — When you trigger a task, the returned handle includes a publicAccessToken that is already scoped to that run.
  2. Generated server-side — Use auth.createPublicToken() to create tokens scoped to specific runs, tags, or batches.
import { auth, tasks } from "@trigger.dev/sdk";

// Trigger a task from your backend (e.g., a Next.js route handler)
const handle = await tasks.trigger("chat", { prompt: "Hello" });

// The handle already contains a public token scoped to this run
const { id: runId, publicAccessToken } = handle;

// Or create a custom-scoped token
const token = await auth.createPublicToken({
  scopes: {
    read: { runs: [handle.id] },
  },
});
Pass the token to your React hooks or backend subscription via the accessToken option.

Subscribing from the backend

You can subscribe to run updates from Node.js, other tasks, or serverless functions using runs.subscribeToRun():
import { runs } from "@trigger.dev/sdk";

const subscription = runs.subscribeToRun(runId);

for await (const run of subscription) {
  console.log("Run status:", run.status);

  if (run.finishedAt) {
    console.log("Run finished with output:", run.output);
    break;
  }
}
To subscribe to all runs with a specific tag:
import { runs } from "@trigger.dev/sdk";

const subscription = runs.subscribeToRunsWithTag(`user:${userId}`);

for await (const run of subscription) {
  console.log("Run update:", run.id, run.status);
}

Next steps

React hooks

Use useRealtimeRun and useRealtimeRunWithStreams in your React components.

Wait for token

Pause a run and resume it from the frontend or an external webhook.

Building with AI

Patterns for LLM chains, agent loops, and human-in-the-loop workflows.

MCP Server

Let your AI assistant interact with your Trigger.dev project.

Build docs developers (and LLMs) love