triggerdotdev/trigger.dev-trigger-realtime-and-frontend
> (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.
npx skills add https://github.com/triggerdotdev/trigger.dev --skill trigger-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.
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.
// 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
});
// 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 <div>Error: {error.message}</div>;
if (!run) return <div>Loading...</div>;
return <div>Run: {run.status}</div>;
}
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.
metadata is Record<string, DeserializedJson>, so nested values need a cast.
"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<typeof myTask>(runId, { accessToken: publicAccessToken });
if (error) return <div>Error: {error.message}</div>;
if (!run) return <div>Loading...</div>;
const progress = run.metadata?.progress as { percentage?: number } | undefined;
return <div>{run.status}: {progress?.percentage ?? 0}%</div>;
}
Pass onComplete: (run, error) => {} to react when the run finishes.
skipColumnsFor a badge or progress bar you do not need payload/output. Skipping them
reduces wire size and avoids "Large HTTP Payload" warnings.
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.
accessToken here is a Trigger Token (auth.createTriggerPublicToken), not a
Public Access Token.
"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<typeof myTask>("my-task", {
accessToken: triggerToken,
});
if (handle) return <div>Run ID: {handle.id}</div>;
return (
<button onClick={() => submit({ foo: "bar" }, { tags: ["user:123"] })} disabled={isLoading}>
{isLoading ? "Triggering..." : "Run"}
</button>
);
}
submit(payload, options?) takes the same options as a backend trigger call.
"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<typeof myTask>("my-task", {
accessToken: publicAccessToken,
});
if (run) return <div>{run.status}</div>;
return <button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>Run</button>;
}
Use useRealtimeTaskTriggerWithStreams<typeof myTask, STREAMS> when you also
want the task's streams (it returns { submit, run, streams, error, isLoading }).
useRealtimeStream takes a defined stream for full type safety, or a runId
plus optional stream key. Returns { parts, error }.
"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 <div>Error: {error.message}</div>;
if (!parts) return <div>Loading...</div>;
return <div>{parts.join("")}</div>;
}
Without a defined stream: useRealtimeStream<string>(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.
"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 (
<button disabled={!isReady || isLoading} onClick={() => send({ approved: true })}>
Approve
</button>
);
}
// 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 };
"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 <button onClick={() => complete({ approved: true })}>Approve</button>;
}
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<typeof myTask>(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.
read token from createPublicToken cannot trigger tasks.
useTaskTrigger("my-task", { accessToken: publicAccessTokenFromCreatePublicToken })auth.createTriggerPublicToken("my-task") and pass that.await auth.createPublicToken()await auth.createPublicToken({ scopes: { read: { runs: ["run_1234"] } } })useRun/SWR for live updates. useRun is the SWR-basedmanagement-API hook (not recommended for live state); set refreshInterval: 0
to stop polling if you do use it.
useRun(runId, { refreshInterval: 1000 }) to track progressuseRealtimeRun(runId, { accessToken }) (no polling, no WebSocket setup)"use client". Realtime/trigger hooks cannot run in a server component.useRealtimeRun"use client"; at the top of any component using these hooks.payload/output you do not render.useRealtimeRun(runId, { accessToken }) for a status badge (large payloads over the wire)useRealtimeRun(runId, { accessToken, skipColumns: ["payload", "output"] })useRealtimeRun(handle, { accessToken: handle?.publicAccessToken }) with no guardenabled: !!handle so it subscribes only once the trigger returns a handle.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)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/.
Take triggerdotdev/trigger.dev-trigger-realtime-and-frontend from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.