--- name: trigger-realtime-and-frontend description: > Trigger.dev client/frontend surface: subscribe to runs in realtime (runs.subscribeToRun and the @trigger.dev/react-hooks hook useRealtimeRun), consume metadata and AI/text streams in React (useRealtimeStream), trigger tasks from the browser (useTaskTrigger, useRealtimeTaskTrigger), and mint scoped frontend credentials with auth.createPublicToken / auth.createTriggerPublicToken. Load when wiring a frontend (React/Next.js/Remix) or backend-for-frontend to show live run progress, status badges, token streams, trigger buttons, or wait-token approval UIs. NOT for writing the backend task itself (streams.define / metadata.set is trigger-authoring-tasks territory); this is the consumer side. type: core library: trigger.dev sources: - docs/realtime/overview.mdx - docs/realtime/how-it-works.mdx - docs/realtime/auth.mdx - docs/realtime/run-object.mdx - docs/realtime/react-hooks/overview.mdx - docs/realtime/react-hooks/subscribe.mdx - docs/realtime/react-hooks/triggering.mdx - docs/realtime/react-hooks/streams.mdx - docs/realtime/react-hooks/swr.mdx - docs/realtime/react-hooks/use-wait-token.mdx - docs/realtime/backend/subscribe.mdx --- # Realtime and Frontend The consumer side of Trigger.dev's run state and streams: read live run updates, render AI/text streams, and trigger tasks from a browser. Hooks come from `@trigger.dev/react-hooks`; token minting and backend subscription come from `@trigger.dev/sdk`. ## Setup ```bash npm add @trigger.dev/react-hooks # frontend hooks (React/Next.js/Remix) # @trigger.dev/sdk is already installed for the backend ``` The flow is always: mint a scoped token in the backend, pass it to the frontend, subscribe with a hook. ```ts // backend (API route / server action) import { auth } from "@trigger.dev/sdk"; const publicAccessToken = await auth.createPublicToken({ scopes: { read: { runs: ["run_1234"] } }, // a token with no scopes is useless }); ``` ```tsx // frontend "use client"; import { useRealtimeRun } from "@trigger.dev/react-hooks"; export function RunStatus({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) { const { run, error } = useRealtimeRun(runId, { accessToken: publicAccessToken }); if (error) return
Error: {error.message}
; if (!run) return
Loading...
; return
Run: {run.status}
; } ``` There are two token kinds: Public Access Tokens (read/subscribe, from `auth.createPublicToken`) and Trigger Tokens (trigger-from-browser, single-use, from `auth.createTriggerPublicToken`). Both default to a 15 minute expiry. ## Core patterns ### 1. Subscribe to a run and render metadata progress `metadata` is `Record`, so nested values need a cast. ```tsx "use client"; import { useRealtimeRun } from "@trigger.dev/react-hooks"; import type { myTask } from "@/trigger/myTask"; export function Progress({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) { const { run, error } = useRealtimeRun(runId, { accessToken: publicAccessToken }); if (error) return
Error: {error.message}
; if (!run) return
Loading...
; const progress = run.metadata?.progress as { percentage?: number } | undefined; return
{run.status}: {progress?.percentage ?? 0}%
; } ``` Pass `onComplete: (run, error) => {}` to react when the run finishes. ### 2. Status-only subscription with `skipColumns` For a badge or progress bar you do not need `payload`/`output`. Skipping them reduces wire size and avoids "Large HTTP Payload" warnings. ```tsx const { run } = useRealtimeRun(runId, { accessToken: publicAccessToken, skipColumns: ["payload", "output"], }); ``` You can skip any of: `payload`, `output`, `metadata`, `startedAt`, `delayUntil`, `queuedAt`, `expiredAt`, `completedAt`, `number`, `isTest`, `usageDurationMs`, `costInCents`, `baseCostInCents`, `ttl`, `payloadType`, `outputType`, `runTags`, `error`. ### 3. Trigger from the browser with a Trigger Token `accessToken` here is a Trigger Token (`auth.createTriggerPublicToken`), not a Public Access Token. ```tsx "use client"; import { useTaskTrigger } from "@trigger.dev/react-hooks"; import type { myTask } from "@/trigger/myTask"; export function TriggerButton({ triggerToken }: { triggerToken: string }) { const { submit, handle, isLoading } = useTaskTrigger("my-task", { accessToken: triggerToken, }); if (handle) return
Run ID: {handle.id}
; return ( ); } ``` `submit(payload, options?)` takes the same options as a backend `trigger` call. ### 4. Trigger and subscribe in one hook ```tsx "use client"; import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks"; import type { myTask } from "@/trigger/myTask"; export function Runner({ publicAccessToken }: { publicAccessToken: string }) { const { submit, run, isLoading } = useRealtimeTaskTrigger("my-task", { accessToken: publicAccessToken, }); if (run) return
{run.status}
; return ; } ``` Use `useRealtimeTaskTriggerWithStreams` when you also want the task's streams (it returns `{ submit, run, streams, error, isLoading }`). ### 5. Consume an AI/text stream (SDK 4.1.0+, recommended) `useRealtimeStream` takes a defined stream for full type safety, or a `runId` plus optional stream key. Returns `{ parts, error }`. ```tsx "use client"; import { useRealtimeStream } from "@trigger.dev/react-hooks"; import { aiStream } from "@/trigger/streams"; // a defined stream -> typed parts export function StreamView({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) { const { parts, error } = useRealtimeStream(aiStream, runId, { accessToken: publicAccessToken, timeoutInSeconds: 300, // default 60 onData: (chunk) => console.log(chunk), }); if (error) return
Error: {error.message}
; if (!parts) return
Loading...
; return
{parts.join("")}
; } ``` Without a defined stream: `useRealtimeStream(runId, "ai-output", { accessToken })`, or omit the key to use the default stream. Other options: `baseURL`, `startIndex`, `throttleInMs` (default 16). The legacy `useRealtimeRunWithStreams(runId, options)` hook is still supported when you need both the run and all its streams at once. ### 6. Send input back into a running task ```tsx "use client"; import { useInputStreamSend } from "@trigger.dev/react-hooks"; import { approval } from "@/trigger/streams"; export function ApprovalForm({ runId, accessToken }: { runId: string; accessToken: string }) { const { send, isLoading, isReady } = useInputStreamSend(approval.id, runId, { accessToken }); return ( ); } ``` ### 7. Complete a wait token from React ```ts // backend: create the token, return id + publicAccessToken to the frontend import { wait } from "@trigger.dev/sdk"; const token = await wait.createToken({ timeout: "10m" }); return { tokenId: token.id, publicToken: token.publicAccessToken }; ``` ```tsx "use client"; import { useWaitToken } from "@trigger.dev/react-hooks"; export function Approve({ tokenId, publicToken }: { tokenId: string; publicToken: string }) { const { complete } = useWaitToken(tokenId, { accessToken: publicToken }); return ; } ``` ### 8. Subscribe from the backend (async iterators) ```ts import { runs, tasks } from "@trigger.dev/sdk"; import type { myTask } from "./trigger/my-task"; const handle = await tasks.trigger("my-task", { some: "data" }); for await (const run of runs.subscribeToRun(handle.id)) { console.log(run.payload.some, run.output?.some); // typed } ``` `runs.subscribeToRun` completes when the run finishes, so the loop exits on its own. ## Common mistakes 1. **CRITICAL: Triggering from the browser with a Public Access Token.** The read token from `createPublicToken` cannot trigger tasks. - Wrong: `useTaskTrigger("my-task", { accessToken: publicAccessTokenFromCreatePublicToken })` - Correct: mint a single-use Trigger Token with `auth.createTriggerPublicToken("my-task")` and pass that. 2. **Token with no scopes.** A scopeless token authorizes nothing, so every subscribe 403s. - Wrong: `await auth.createPublicToken()` - Correct: `await auth.createPublicToken({ scopes: { read: { runs: ["run_1234"] } } })` 3. **Polling with `useRun`/SWR for live updates.** `useRun` is the SWR-based management-API hook (not recommended for live state); set `refreshInterval: 0` to stop polling if you do use it. - Wrong: `useRun(runId, { refreshInterval: 1000 })` to track progress - Correct: `useRealtimeRun(runId, { accessToken })` (no polling, no WebSocket setup) 4. **Forgetting `"use client"`.** Realtime/trigger hooks cannot run in a server component. - Wrong: a Next.js App Router server component using `useRealtimeRun` - Correct: put `"use client";` at the top of any component using these hooks. 5. **Shipping `payload`/`output` you do not render.** - Wrong: `useRealtimeRun(runId, { accessToken })` for a status badge (large payloads over the wire) - Correct: `useRealtimeRun(runId, { accessToken, skipColumns: ["payload", "output"] })` 6. **Subscribing before the handle exists.** - Wrong: `useRealtimeRun(handle, { accessToken: handle?.publicAccessToken })` with no guard - Correct: add `enabled: !!handle` so it subscribes only once the trigger returns a handle. ## References Sibling skills: - `trigger-authoring-tasks` for the task side: `streams.define()`, `metadata.set()`, and `wait.createToken`. - `trigger-authoring-chat-agent` and `trigger-chat-agent-advanced` for chat agents, which build on these realtime streams. Reference docs ship beside this skill in the same package, read them locally (no network), pinned to your installed version. The `sources:` frontmatter above lists every doc this skill draws from, all under `@trigger.dev/sdk/docs/`. Start with: - `@trigger.dev/sdk/docs/realtime/react-hooks/subscribe.mdx` - `@trigger.dev/sdk/docs/realtime/react-hooks/streams.mdx` - `@trigger.dev/sdk/docs/realtime/auth.mdx` - `@trigger.dev/sdk/docs/realtime/run-object.mdx` (the realtime run object differs from the management-API object returned by `useRun`) ## Version This skill is bundled inside `@trigger.dev/sdk` and read directly from `node_modules`, so it always matches your installed SDK version (see the adjacent `package.json`). The full documentation for these APIs ships alongside it under `@trigger.dev/sdk/docs/`.