Skip to main content
Subscribe to a run and your component re-renders whenever its status, metadata, or tags change. Build progress bars, deployment monitors, or any UI that needs to reflect what a background task is doing right now. For streaming continuous data (AI tokens, file chunks), see Streaming instead.

Trigger + subscribe combo hooks

Trigger a task and immediately subscribe to its run. Details in the triggering section.

Subscribe hooks

useRealtimeRun

The useRealtimeRun hook allows you to subscribe to a run by its ID.
"use client"; // This is needed for Next.js App Router or other RSC frameworks

import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function MyComponent({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  if (error) return <div>Error: {error.message}</div>;

  return <div>Run: {run.id}</div>;
}
To correctly type the run’s payload and output, you can provide the type of your task to the useRealtimeRun hook:
import { useRealtimeRun } from "@trigger.dev/react-hooks";
import type { myTask } from "@/trigger/myTask";

export function MyComponent({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error } = useRealtimeRun<typeof myTask>(runId, {
    accessToken: publicAccessToken,
  });

  if (error) return <div>Error: {error.message}</div>;

  // Now run.payload and run.output are correctly typed

  return <div>Run: {run.id}</div>;
}
You can supply an onComplete callback to the useRealtimeRun hook to be called when the run is completed or errored. This is useful if you want to perform some action when the run is completed, like navigating to a different page or showing a notification.
import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function MyComponent({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
    onComplete: (run, error) => {
      console.log("Run completed", run);
    },
  });

  if (error) return <div>Error: {error.message}</div>;

  return <div>Run: {run.id}</div>;
}
When you only need run status (for example, a progress bar or completion badge), you can omit large fields like payload and output by passing skipColumns. This reduces the data sent over the wire and avoids issues such as “Large HTTP Payload” warnings in tools like Sentry.
import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function RunStatusBadge({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
    skipColumns: ["payload", "output"],
  });

  if (error) return <span>Error</span>;
  if (!run) return <span>Loading…</span>;

  return <span>Status: {run.status}</span>;
}
You can skip any of: payload, output, metadata, startedAt, delayUntil, queuedAt, expiredAt, completedAt, number, isTest, usageDurationMs, costInCents, baseCostInCents, ttl, payloadType, outputType, runTags, error. The useRealtimeRunsWithTag hook also accepts a skipColumns option in the same way. See our run object reference for the complete schema and How it Works documentation for more technical details.

useRealtimeRunsWithTag

The useRealtimeRunsWithTag hook allows you to subscribe to multiple runs with a specific tag.
"use client"; // This is needed for Next.js App Router or other RSC frameworks

import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";

export function MyComponent({ tag }: { tag: string }) {
  const { runs, error } = useRealtimeRunsWithTag(tag);

  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {runs.map((run) => (
        <div key={run.id}>Run: {run.id}</div>
      ))}
    </div>
  );
}
To correctly type the runs payload and output, you can provide the type of your task to the useRealtimeRunsWithTag hook:
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
import type { myTask } from "@/trigger/myTask";

export function MyComponent({ tag }: { tag: string }) {
  const { runs, error } = useRealtimeRunsWithTag<typeof myTask>(tag);

  if (error) return <div>Error: {error.message}</div>;

  // Now runs[i].payload and runs[i].output are correctly typed

  return (
    <div>
      {runs.map((run) => (
        <div key={run.id}>Run: {run.id}</div>
      ))}
    </div>
  );
}
If useRealtimeRunsWithTag could return multiple different types of tasks, you can pass a union of all the task types to the hook:
import { useRealtimeRunsWithTag } from "@trigger.dev/react-hooks";
import type { myTask1, myTask2 } from "@/trigger/myTasks";

export function MyComponent({ tag }: { tag: string }) {
  const { runs, error } = useRealtimeRunsWithTag<typeof myTask1 | typeof myTask2>(tag);

  if (error) return <div>Error: {error.message}</div>;

  // You can narrow down the type of the run based on the taskIdentifier
  for (const run of runs) {
    if (run.taskIdentifier === "my-task-1") {
      // run is correctly typed as myTask1
    } else if (run.taskIdentifier === "my-task-2") {
      // run is correctly typed as myTask2
    }
  }

  return (
    <div>
      {runs.map((run) => (
        <div key={run.id}>Run: {run.id}</div>
      ))}
    </div>
  );
}

useRealtimeBatch

The useRealtimeBatch hook allows you to subscribe to a batch of runs by its the batch ID.
"use client"; // This is needed for Next.js App Router or other RSC frameworks

import { useRealtimeBatch } from "@trigger.dev/react-hooks";

export function MyComponent({ batchId }: { batchId: string }) {
  const { runs, error } = useRealtimeBatch(batchId);

  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      {runs.map((run) => (
        <div key={run.id}>Run: {run.id}</div>
      ))}
    </div>
  );
}
See our Realtime documentation for more information.

Using metadata to show progress in your UI

All realtime hooks automatically include metadata updates. Whenever your task updates metadata using metadata.set(), metadata.append(), or other metadata methods, your component will re-render with the updated data.
To learn how to write tasks using metadata, see our metadata guide.

Progress monitoring

This example demonstrates how to create a progress monitor component that can be used to display the progress of a run:
"use client"; // This is needed for Next.js App Router or other RSC frameworks

import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function ProgressMonitor({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error, isLoading } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  if (isLoading) return <div>Loading run...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!run) return <div>Run not found</div>;

  const progress = run.metadata?.progress as
    | {
        current: number;
        total: number;
        percentage: number;
        currentItem: string;
      }
    | undefined;

  return (
    <div className="space-y-4">
      <div>
        <h3>Run Status: {run.status}</h3>
        <p>Run ID: {run.id}</p>
      </div>

      {progress && (
        <div className="space-y-2">
          <div className="flex justify-between text-sm">
            <span>Progress</span>
            <span>{progress.percentage}%</span>
          </div>
          <div className="w-full bg-gray-200 rounded-full h-2">
            <div
              className="bg-blue-600 h-2 rounded-full transition-all duration-300"
              style={{ width: `${progress.percentage}%` }}
            />
          </div>
          <p className="text-sm text-gray-600">
            Processing: {progress.currentItem} ({progress.current}/{progress.total})
          </p>
        </div>
      )}
    </div>
  );
}

Reusable progress bar

This example demonstrates how to create a reusable progress bar component that can be used to display the percentage progress of a run:
"use client";

import { useRealtimeRun } from "@trigger.dev/react-hooks";

interface ProgressBarProps {
  runId: string;
  publicAccessToken: string;
  title?: string;
}

export function ProgressBar({ runId, publicAccessToken, title }: ProgressBarProps) {
  const { run } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  const progress = run?.metadata?.progress as
    | {
        current?: number;
        total?: number;
        percentage?: number;
        currentItem?: string;
      }
    | undefined;

  const percentage = progress?.percentage ?? 0;
  const isComplete = run?.status === "COMPLETED";
  const isFailed = run?.status === "FAILED";

  return (
    <div className="w-full space-y-2">
      {title && <h4 className="font-medium">{title}</h4>}

      <div className="w-full bg-gray-200 rounded-full h-3">
        <div
          className={`h-3 rounded-full transition-all duration-500 ${
            isFailed ? "bg-red-500" : isComplete ? "bg-green-500" : "bg-blue-500"
          }`}
          style={{ width: `${percentage}%` }}
        />
      </div>

      <div className="flex justify-between text-sm text-gray-600">
        <span>
          {progress?.current && progress?.total
            ? `${progress.current}/${progress.total} items`
            : "Processing..."}
        </span>
        <span>{percentage}%</span>
      </div>

      {progress?.currentItem && (
        <p className="text-sm text-gray-500 truncate">Current: {progress.currentItem}</p>
      )}
    </div>
  );
}

Status indicator with logs

This example demonstrates how to create a status indicator component that can be used to display the status of a run, and also logs that are emitted by the task:
"use client";

import { useRealtimeRun } from "@trigger.dev/react-hooks";

interface StatusIndicatorProps {
  runId: string;
  publicAccessToken: string;
}

export function StatusIndicator({ runId, publicAccessToken }: StatusIndicatorProps) {
  const { run } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  const status = run?.metadata?.status as string | undefined;
  const logs = run?.metadata?.logs as string[] | undefined;

  const getStatusColor = (status: string | undefined) => {
    switch (status) {
      case "completed":
        return "text-green-600 bg-green-100";
      case "failed":
        return "text-red-600 bg-red-100";
      case "running":
        return "text-blue-600 bg-blue-100";
      default:
        return "text-gray-600 bg-gray-100";
    }
  };

  return (
    <div className="space-y-4">
      <div className="flex items-center space-x-2">
        <span className={`px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(status)}`}>
          {status || run?.status || "Unknown"}
        </span>
        <span className="text-sm text-gray-500">Run {run?.id}</span>
      </div>

      {logs && logs.length > 0 && (
        <div className="bg-gray-50 rounded-lg p-4">
          <h4 className="font-medium mb-2">Logs</h4>
          <div className="space-y-1 max-h-48 overflow-y-auto">
            {logs.map((log, index) => (
              <div key={index} className="text-sm text-gray-700 font-mono">
                {log}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Multi-stage deployment monitor

This example demonstrates how to create a multi-stage deployment monitor component that can be used to display the progress of a deployment:
"use client";

import { useRealtimeRun } from "@trigger.dev/react-hooks";

interface DeploymentMonitorProps {
  runId: string;
  publicAccessToken: string;
}

const DEPLOYMENT_STAGES = [
  "initializing",
  "building",
  "testing",
  "deploying",
  "verifying",
  "completed",
] as const;

export function DeploymentMonitor({ runId, publicAccessToken }: DeploymentMonitorProps) {
  const { run } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  const status = run?.metadata?.status as string | undefined;
  const logs = run?.metadata?.logs as string[] | undefined;
  const currentStageIndex = DEPLOYMENT_STAGES.indexOf(status as any);

  return (
    <div className="space-y-6">
      <h3 className="text-lg font-semibold">Deployment Progress</h3>

      {/* Stage indicators */}
      <div className="space-y-4">
        {DEPLOYMENT_STAGES.map((stage, index) => {
          const isActive = currentStageIndex === index;
          const isCompleted = currentStageIndex > index;
          const isFailed = run?.status === "FAILED" && currentStageIndex === index;

          return (
            <div key={stage} className="flex items-center space-x-3">
              <div
                className={`w-6 h-6 rounded-full flex items-center justify-center text-sm font-medium ${
                  isFailed
                    ? "bg-red-500 text-white"
                    : isCompleted
                    ? "bg-green-500 text-white"
                    : isActive
                    ? "bg-blue-500 text-white"
                    : "bg-gray-200 text-gray-600"
                }`}
              >
                {isCompleted ? "✓" : index + 1}
              </div>
              <span
                className={`capitalize ${
                  isActive
                    ? "font-medium text-blue-600"
                    : isCompleted
                    ? "text-green-600"
                    : isFailed
                    ? "text-red-600"
                    : "text-gray-500"
                }`}
              >
                {stage}
              </span>
              {isActive && (
                <div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full" />
              )}
            </div>
          );
        })}
      </div>

      {/* Recent logs */}
      {logs && logs.length > 0 && (
        <div className="bg-black text-green-400 rounded-lg p-4 font-mono text-sm">
          <div className="space-y-1 max-h-32 overflow-y-auto">
            {logs.slice(-5).map((log, index) => (
              <div key={index}>
                <span className="text-gray-500">$ </span>
                {log}
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

Type safety

Define TypeScript interfaces for your metadata to get full type safety:
"use client";

import { useRealtimeRun } from "@trigger.dev/react-hooks";

interface TaskMetadata {
  progress?: {
    current: number;
    total: number;
    percentage: number;
    currentItem: string;
  };
  status?: "initializing" | "processing" | "completed" | "failed";
  user?: {
    id: string;
    name: string;
  };
  logs?: string[];
}

export function TypedMetadataComponent({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
  });

  // Type-safe metadata access
  const metadata = run?.metadata as TaskMetadata | undefined;

  return (
    <div>
      {metadata?.progress && <p>Progress: {metadata.progress.percentage}%</p>}

      {metadata?.user && (
        <p>
          User: {metadata.user.name} ({metadata.user.id})
        </p>
      )}

      {metadata?.status && <p>Status: {metadata.status}</p>}
    </div>
  );
}

Common options

accessToken & baseURL

You can pass the accessToken option to the Realtime hooks to authenticate the subscription.
import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function MyComponent({
  runId,
  publicAccessToken,
}: {
  runId: string;
  publicAccessToken: string;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
    baseURL: "https://my-self-hosted-trigger.com", // Optional if you are using a self-hosted Trigger.dev instance
  });

  if (error) return <div>Error: {error.message}</div>;

  return <div>Run: {run.id}</div>;
}

enabled

You can pass the enabled option to the Realtime hooks to enable or disable the subscription.
import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function MyComponent({
  runId,
  publicAccessToken,
  enabled,
}: {
  runId: string;
  publicAccessToken: string;
  enabled: boolean;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
    enabled,
  });

  if (error) return <div>Error: {error.message}</div>;

  return <div>Run: {run.id}</div>;
}
This allows you to conditionally disable using the hook based on some state.

id

You can pass the id option to the Realtime hooks to change the ID of the subscription.
import { useRealtimeRun } from "@trigger.dev/react-hooks";

export function MyComponent({
  id,
  runId,
  publicAccessToken,
  enabled,
}: {
  id: string;
  runId: string;
  publicAccessToken: string;
  enabled: boolean;
}) {
  const { run, error } = useRealtimeRun(runId, {
    accessToken: publicAccessToken,
    enabled,
    id,
  });

  if (error) return <div>Error: {error.message}</div>;

  return <div>Run: {run.id}</div>;
}
This allows you to change the ID of the subscription based on some state. Passing in a different ID will unsubscribe from the current subscription and subscribe to the new one (and remove any cached data).

Frequently asked questions

How do I show a progress bar for a background task in React?

Use metadata.set() inside your task to update a progress value, then read it with useRealtimeRun in your component. The hook re-renders your component on every metadata change. See Using metadata to show progress above for a complete example.

What’s the difference between run updates and streaming?

Run updates (this page) give you run state: status, metadata, and tags. They’re for progress bars, status badges, and dashboards. Streaming gives you continuous data like AI tokens or file chunks. Use run updates for “how far along is my task?” and streaming for “show me the output as it generates.”

Can I subscribe to multiple runs at once?

Yes. Use useRealtimeRunsWithTag to subscribe to all runs with a specific tag (e.g., user:123), or useRealtimeBatch for all runs in a batch. Each yields an array of run objects that update in real time.

Do I need to set up polling or WebSockets?

No. The hooks handle the connection automatically using Trigger.dev’s Realtime infrastructure (built on Electric SQL). Just pass a run ID and an access token.