chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
---
|
||||
title: Realtime authentication
|
||||
sidebarTitle: Realtime auth
|
||||
description: Authenticating real-time API requests with Public Access Tokens or Trigger Tokens
|
||||
---
|
||||
|
||||
To use the Realtime API, you need to authenticate your requests with Public Access Tokens or Trigger Tokens. These tokens provide secure, scoped access to your runs and can be used in both frontend and backend applications.
|
||||
|
||||
## Token Types
|
||||
|
||||
There are two types of tokens you can use with the Realtime API:
|
||||
|
||||
- **[Public Access Tokens](#public-access-tokens-for-subscribing-to-runs)** - Used to read and subscribe to run data. Can be used in both the frontend and backend.
|
||||
- **[Trigger Tokens](#trigger-tokens-for-frontend-triggering-only)** - Used to trigger tasks from your frontend. These are more secure, single-use tokens that can only be used in the frontend.
|
||||
|
||||
## Public Access Tokens (for subscribing to runs)
|
||||
|
||||
Use Public Access Tokens to subscribe to runs and receive real-time updates in your frontend or backend.
|
||||
|
||||
### Creating Public Access Tokens
|
||||
|
||||
You can create a Public Access Token using the `auth.createPublicToken` function in your **backend** code:
|
||||
|
||||
```tsx
|
||||
// Somewhere in your backend code
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken(); // 👈 this public access token has no permissions, so is pretty useless!
|
||||
```
|
||||
|
||||
### Scopes
|
||||
|
||||
By default a Public Access Token has no permissions. You must specify the scopes you need when creating a Public Access Token:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: true, // ❌ this token can read all runs, possibly useful for debugging/testing
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This will allow the token to read all runs, which is probably not what you want. You can specify only certain runs by passing an array of run IDs:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
runs: ["run_1234", "run_5678"], // ✅ this token can read only these runs
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can scope the token to only read certain tasks:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
tasks: ["my-task-1", "my-task-2"], // 👈 this token can read all runs of these tasks
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Or tags:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
tags: ["my-tag-1", "my-tag-2"], // 👈 this token can read all runs with these tags
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Or a specific batch of runs:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
batch: "batch_1234", // 👈 this token can read all runs in this batch
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also combine scopes. For example, to read runs with specific tags and for specific tasks:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
scopes: {
|
||||
read: {
|
||||
tasks: ["my-task-1", "my-task-2"],
|
||||
tags: ["my-tag-1", "my-tag-2"],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Expiration
|
||||
|
||||
By default, Public Access Token's expire after 15 minutes. You can specify a different expiration time when creating a Public Access Token:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
const publicToken = await auth.createPublicToken({
|
||||
expirationTime: "1hr",
|
||||
});
|
||||
```
|
||||
|
||||
- If `expirationTime` is a string, it will be treated as a time span
|
||||
- If `expirationTime` is a number, it will be treated as a Unix timestamp
|
||||
- If `expirationTime` is a `Date`, it will be treated as a date
|
||||
|
||||
The format used for a time span is the same as the [jose package](https://github.com/panva/jose), which is a number followed by a unit. Valid units are: "sec", "secs", "second", "seconds", "s", "minute", "minutes", "min", "mins", "m", "hour", "hours", "hr", "hrs", "h", "day", "days", "d", "week", "weeks", "w", "year", "years", "yr", "yrs", and "y". It is not possible to specify months. 365.25 days is used as an alias for a year. If the string is suffixed with "ago", or prefixed with a "-", the resulting time span gets subtracted from the current unix timestamp. A "from now" suffix can also be used for readability when adding to the current unix timestamp.
|
||||
|
||||
### Auto-generated tokens
|
||||
|
||||
When you [trigger tasks](/triggering) from your backend, the `handle` received includes a `publicAccessToken` field. This token can be used to authenticate real-time requests in your frontend application.
|
||||
|
||||
By default, auto-generated tokens expire after 15 minutes and have a read scope for the specific run(s) that were triggered. You can customize the expiration by passing a `publicTokenOptions` object to the trigger function.
|
||||
|
||||
See our [triggering documentation](/triggering) for detailed examples of how to trigger tasks and get auto-generated tokens.
|
||||
|
||||
<Note>
|
||||
**Where should I create tokens?** The standard pattern is to create tokens in your backend code (API route, server action) after triggering a task, then pass the token to your frontend. The `handle.publicAccessToken` returned by `tasks.trigger()` already does this for you. You rarely need to create tokens inside a task itself.
|
||||
</Note>
|
||||
|
||||
### Subscribing to runs with Public Access Tokens
|
||||
|
||||
Once you have a Public Access Token, you can use it to authenticate requests to the Realtime API in both backend and frontend applications.
|
||||
|
||||
**Backend usage:** See our [backend documentation](/realtime/backend) for examples of what you can do with Realtime in your backend once you have authenticated with a token.
|
||||
|
||||
**Frontend usage:** See our [React hooks documentation](/realtime/react-hooks) for examples of using tokens with frontend components.
|
||||
|
||||
## Trigger Tokens (for frontend triggering only)
|
||||
|
||||
For triggering tasks from your frontend, you need special "trigger" tokens. These tokens can only be used once to trigger a task and are more secure than regular Public Access Tokens.
|
||||
|
||||
### Creating Trigger Tokens
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task");
|
||||
```
|
||||
|
||||
### Multiple tasks
|
||||
|
||||
You can pass multiple tasks to create a token that can trigger multiple tasks:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken(["my-task-1", "my-task-2"]);
|
||||
```
|
||||
|
||||
### Multiple use
|
||||
|
||||
You can also create tokens that can be used multiple times:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task", {
|
||||
multipleUse: true, // ❌ Use this with caution!
|
||||
});
|
||||
```
|
||||
|
||||
### Expiration
|
||||
|
||||
These tokens also expire, with the default expiration time being 15 minutes. You can specify a custom expiration time:
|
||||
|
||||
```ts
|
||||
import { auth } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
const triggerToken = await auth.createTriggerPublicToken("my-task", {
|
||||
expirationTime: "24hr",
|
||||
});
|
||||
```
|
||||
|
||||
### Triggering tasks from the frontend with Trigger Tokens
|
||||
|
||||
Check out our [React hooks documentation](/realtime/react-hooks) for examples of how to use Trigger Tokens in your frontend applications.
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
title: Input Streams
|
||||
sidebarTitle: Input Streams
|
||||
description: Send data into running tasks from your backend code
|
||||
---
|
||||
|
||||
The Input Streams API allows you to send data into running Trigger.dev tasks from your backend code. This enables bidirectional communication — while [output streams](/realtime/backend/streams) let you read data from tasks, input streams let you push data into them.
|
||||
|
||||
<Note>
|
||||
To learn how to receive input stream data inside your tasks, see [Input
|
||||
Streams](/tasks/streams#input-streams) in the Streams doc.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
Input streams are keyed by `runId` — they're correct for sending data to a specific live run. If you need a bidirectional channel that survives run boundaries (e.g. a chat that resumes tomorrow, an agent coordinated across many runs), look at [`chat.agent`](/ai-chat/overview): it's built on a durable Session row that owns its runs and exposes the same consumer-side API (`on` / `once` / `wait` / `waitWithIdleTimeout`) on its `.in` channel.
|
||||
</Tip>
|
||||
|
||||
## Sending data to a running task
|
||||
|
||||
### Using defined input streams (Recommended)
|
||||
|
||||
The recommended approach is to use [defined input streams](/tasks/streams#defining-input-streams) for full type safety:
|
||||
|
||||
```ts
|
||||
import { cancelSignal, approval } from "./trigger/streams";
|
||||
|
||||
// Cancel a running AI stream
|
||||
await cancelSignal.send(runId, { reason: "User clicked stop" });
|
||||
|
||||
// Approve a draft
|
||||
await approval.send(runId, { approved: true, reviewer: "alice@example.com" });
|
||||
```
|
||||
|
||||
The `.send()` method is fully typed — the data parameter must match the generic type you defined on the input stream.
|
||||
|
||||
<Note>
|
||||
`.send()` works the same regardless of how the task is listening — whether it uses `.wait()`
|
||||
(suspending), `.once()` (non-suspending), or `.on()` (continuous). The sender doesn't need to know
|
||||
how the task is consuming the data. See [Input Streams](/tasks/streams#input-streams) for details on each
|
||||
receiving method.
|
||||
</Note>
|
||||
|
||||
## Practical examples
|
||||
|
||||
### Cancel from a Next.js API route
|
||||
|
||||
```ts app/api/cancel/route.ts
|
||||
import { cancelStream } from "@/trigger/streams";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { runId } = await req.json();
|
||||
|
||||
await cancelStream.send(runId, { reason: "User clicked stop" });
|
||||
|
||||
return Response.json({ cancelled: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Approval workflow API
|
||||
|
||||
```ts app/api/approve/route.ts
|
||||
import { approval } from "@/trigger/streams";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { runId, approved, reviewer } = await req.json();
|
||||
|
||||
await approval.send(runId, {
|
||||
approved,
|
||||
reviewer,
|
||||
});
|
||||
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Remix action handler
|
||||
|
||||
```ts app/routes/api.approve.ts
|
||||
import { json, type ActionFunctionArgs } from "@remix-run/node";
|
||||
import { approval } from "~/trigger/streams";
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const formData = await request.formData();
|
||||
const runId = formData.get("runId") as string;
|
||||
const approved = formData.get("approved") === "true";
|
||||
const reviewer = formData.get("reviewer") as string;
|
||||
|
||||
await approval.send(runId, { approved, reviewer });
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
### Express handler
|
||||
|
||||
```ts
|
||||
import express from "express";
|
||||
import { cancelSignal } from "./trigger/streams";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.post("/api/cancel", async (req, res) => {
|
||||
const { runId, reason } = req.body;
|
||||
|
||||
await cancelSignal.send(runId, { reason });
|
||||
|
||||
res.json({ cancelled: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Sending from another task
|
||||
|
||||
You can send input stream data from one task to another running task:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { approval } from "./streams";
|
||||
|
||||
export const reviewerTask = task({
|
||||
id: "auto-reviewer",
|
||||
run: async (payload: { targetRunId: string }) => {
|
||||
// Perform automated review logic...
|
||||
const isApproved = await performReview();
|
||||
|
||||
// Send approval to the waiting task
|
||||
await approval.send(payload.targetRunId, {
|
||||
approved: isApproved,
|
||||
reviewer: "auto-reviewer",
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
The `.send()` method will throw if:
|
||||
|
||||
- The run has already completed, failed, or been canceled
|
||||
- The payload exceeds the 1MB size limit
|
||||
- The run ID is invalid
|
||||
|
||||
```ts
|
||||
import { cancelSignal } from "./trigger/streams";
|
||||
|
||||
try {
|
||||
await cancelSignal.send(runId, { reason: "User clicked stop" });
|
||||
} catch (error) {
|
||||
console.error("Failed to send:", error);
|
||||
// Handle the error — the run may have already completed
|
||||
}
|
||||
```
|
||||
|
||||
## Important notes
|
||||
|
||||
- Maximum payload size per `.send()` call is **1MB**
|
||||
- You cannot send data to a completed, failed, or canceled run
|
||||
- Data sent before a listener is registered inside the task is **buffered** and delivered when a listener attaches
|
||||
- Input streams require the current streams implementation (v2 is the default in SDK 4.1.0+). See [Streams](/tasks/streams) for details.
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: "Subscribe to tasks from your backend"
|
||||
sidebarTitle: Overview
|
||||
description: "Subscribe to run progress, stream AI output, and react to task status changes from your backend code or other tasks."
|
||||
---
|
||||
|
||||
import RealtimeExamplesCards from "/snippets/realtime-examples-cards.mdx";
|
||||
|
||||
**Subscribe to runs from your server-side code or other tasks using async iterators.** Get status updates, metadata changes, and streamed data without polling.
|
||||
|
||||
## What's available
|
||||
|
||||
| Category | What it does | Guide |
|
||||
|---|---|---|
|
||||
| **Run updates** | Subscribe to run status, metadata, and tag changes | [Run updates](/realtime/backend/subscribe) |
|
||||
| **Streaming** | Read AI output, file chunks, or any continuous data from tasks | [Streaming](/realtime/backend/streams) |
|
||||
|
||||
<Note>
|
||||
To learn how to emit streams from your tasks, see [Streaming data from tasks](/tasks/streams).
|
||||
</Note>
|
||||
|
||||
## Authentication
|
||||
|
||||
All backend functions support both server-side and client-side authentication:
|
||||
|
||||
- **Server-side**: Use your API key (automatically handled in tasks)
|
||||
- **Client-side**: Generate a Public Access Token with appropriate scopes
|
||||
|
||||
See our [authentication guide](/realtime/auth) for detailed information on creating and using tokens.
|
||||
|
||||
## Quick example
|
||||
|
||||
Subscribe to a run:
|
||||
|
||||
```ts
|
||||
import { runs, tasks } from "@trigger.dev/sdk";
|
||||
|
||||
// Trigger a task
|
||||
const handle = await tasks.trigger("my-task", { some: "data" });
|
||||
|
||||
// Subscribe to real-time updates
|
||||
for await (const run of runs.subscribeToRun(handle.id)) {
|
||||
console.log(`Run ${run.id} status: ${run.status}`);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,418 @@
|
||||
---
|
||||
title: "Stream data to your backend (AI, files)"
|
||||
sidebarTitle: "Streaming"
|
||||
description: "Read AI/LLM output, file chunks, and other streaming data from your Trigger.dev tasks in backend code."
|
||||
---
|
||||
|
||||
**Read streaming data from your tasks in backend code.** Consume AI completions as they generate, process file chunks, or handle any continuous data your tasks produce.
|
||||
|
||||
<Note>
|
||||
To emit streams from your tasks, see [Streaming data from tasks](/tasks/streams). For React components, see [Streaming in React](/realtime/react-hooks/streams).
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
Run-scoped streams are the right primitive for ephemeral I/O that lives inside a single run's lifetime. For durable, long-lived channels that outlive a run, see [`chat.agent`](/ai-chat/overview): it's built on a Session row that owns the chat's runs and exposes bidirectional `.in` / `.out` channels addressed by a durable id.
|
||||
</Tip>
|
||||
|
||||
## Reading streams
|
||||
|
||||
### Using defined streams (Recommended)
|
||||
|
||||
The recommended approach is to use [defined streams](/tasks/streams#defining-typed-streams-recommended) for full type safety:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function consumeStream(runId: string) {
|
||||
// Read from the defined stream
|
||||
const stream = await aiStream.read(runId);
|
||||
|
||||
let fullText = "";
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk); // chunk is typed!
|
||||
fullText += chunk;
|
||||
}
|
||||
|
||||
console.log("Final text:", fullText);
|
||||
}
|
||||
```
|
||||
|
||||
### Direct stream reading
|
||||
|
||||
If you prefer not to use defined streams, you can read directly by specifying the stream key:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
|
||||
async function consumeStream(runId: string) {
|
||||
// Read from a stream by key
|
||||
const stream = await streams.read<string>(runId, "ai-output");
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Reading from the default stream
|
||||
|
||||
Every run has a default stream, so you can omit the stream key:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
|
||||
async function consumeDefaultStream(runId: string) {
|
||||
// Read from the default stream
|
||||
const stream = await streams.read<string>(runId);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Stream options
|
||||
|
||||
The `read()` method accepts several options for controlling stream behavior:
|
||||
|
||||
### Timeout
|
||||
|
||||
Set a timeout to stop reading if no data is received within a specified time:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function consumeWithTimeout(runId: string) {
|
||||
const stream = await aiStream.read(runId, {
|
||||
timeoutInSeconds: 120, // Wait up to 2 minutes for data
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "TimeoutError") {
|
||||
console.log("Stream timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Start index
|
||||
|
||||
Resume reading from a specific chunk index (useful for reconnection scenarios):
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function resumeStream(runId: string, lastChunkIndex: number) {
|
||||
// Start reading from the chunk after the last one we received
|
||||
const stream = await aiStream.read(runId, {
|
||||
startIndex: lastChunkIndex + 1,
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Abort signal
|
||||
|
||||
Use an `AbortSignal` to cancel stream reading:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function consumeWithCancellation(runId: string) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// Cancel after 30 seconds
|
||||
setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
const stream = await aiStream.read(runId, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
|
||||
// Optionally abort based on content
|
||||
if (chunk.includes("STOP")) {
|
||||
controller.abort();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Stream was cancelled");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Combining options
|
||||
|
||||
You can combine multiple options:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function advancedStreamConsumption(runId: string) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const stream = await aiStream.read(runId, {
|
||||
timeoutInSeconds: 300, // 5 minute timeout
|
||||
startIndex: 0, // Start from the beginning
|
||||
signal: controller.signal, // Allow cancellation
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("Stream was cancelled");
|
||||
} else if (error.name === "TimeoutError") {
|
||||
console.log("Stream timed out");
|
||||
} else {
|
||||
console.error("Stream error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Practical examples
|
||||
|
||||
### Reading AI streaming responses
|
||||
|
||||
Here's a complete example of consuming an AI stream from your backend:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function consumeAIStream(runId: string) {
|
||||
const stream = await aiStream.read(runId, {
|
||||
timeoutInSeconds: 300, // AI responses can take time
|
||||
});
|
||||
|
||||
let fullResponse = "";
|
||||
const chunks: string[] = [];
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
fullResponse += chunk;
|
||||
|
||||
// Process each chunk as it arrives
|
||||
console.log("Chunk received:", chunk);
|
||||
|
||||
// Could send to websocket, SSE, etc.
|
||||
// await sendToClient(chunk);
|
||||
}
|
||||
|
||||
console.log("Stream complete!");
|
||||
console.log("Total chunks:", chunks.length);
|
||||
console.log("Full response:", fullResponse);
|
||||
|
||||
return { fullResponse, chunks };
|
||||
}
|
||||
```
|
||||
|
||||
### Reading multiple streams
|
||||
|
||||
If a task emits multiple streams, you can read them concurrently or sequentially:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream, progressStream } from "./trigger/streams";
|
||||
|
||||
async function consumeMultipleStreams(runId: string) {
|
||||
// Read streams concurrently
|
||||
const [aiData, progressData] = await Promise.all([
|
||||
consumeStream(aiStream, runId),
|
||||
consumeStream(progressStream, runId),
|
||||
]);
|
||||
|
||||
return { aiData, progressData };
|
||||
}
|
||||
|
||||
async function consumeStream<T>(
|
||||
streamDef: { read: (runId: string) => Promise<AsyncIterableStream<T>> },
|
||||
runId: string
|
||||
): Promise<T[]> {
|
||||
const stream = await streamDef.read(runId);
|
||||
const chunks: T[] = [];
|
||||
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
```
|
||||
|
||||
### Piping streams to HTTP responses
|
||||
|
||||
You can pipe streams directly to HTTP responses for server-sent events (SSE):
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const runId = request.nextUrl.searchParams.get("runId");
|
||||
|
||||
if (!runId) {
|
||||
return new Response("Missing runId", { status: 400 });
|
||||
}
|
||||
|
||||
const stream = await aiStream.read(runId, {
|
||||
timeoutInSeconds: 300,
|
||||
});
|
||||
|
||||
// Create a readable stream for SSE
|
||||
const encoder = new TextEncoder();
|
||||
const readableStream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
// Format as SSE
|
||||
const data = `data: ${JSON.stringify({ chunk })}\n\n`;
|
||||
controller.enqueue(encoder.encode(data));
|
||||
}
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(readableStream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing retry logic
|
||||
|
||||
Handle transient errors with retry logic:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function consumeStreamWithRetry(
|
||||
runId: string,
|
||||
maxRetries = 3
|
||||
): Promise<string[]> {
|
||||
let lastChunkIndex = 0;
|
||||
const allChunks: string[] = [];
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const stream = await aiStream.read(runId, {
|
||||
startIndex: lastChunkIndex,
|
||||
timeoutInSeconds: 120,
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
allChunks.push(chunk);
|
||||
lastChunkIndex++;
|
||||
}
|
||||
|
||||
// Success! Break out of retry loop
|
||||
break;
|
||||
} catch (error) {
|
||||
attempt++;
|
||||
|
||||
if (attempt >= maxRetries) {
|
||||
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log(`Retry attempt ${attempt} after error:`, error.message);
|
||||
|
||||
// Wait before retrying (exponential backoff)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
||||
}
|
||||
}
|
||||
|
||||
return allChunks;
|
||||
}
|
||||
```
|
||||
|
||||
### Processing streams in chunks
|
||||
|
||||
Process streams in batches for efficiency:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./trigger/streams";
|
||||
|
||||
async function processStreamInBatches(runId: string, batchSize = 10) {
|
||||
const stream = await aiStream.read(runId);
|
||||
|
||||
let batch: string[] = [];
|
||||
|
||||
for await (const chunk of stream) {
|
||||
batch.push(chunk);
|
||||
|
||||
if (batch.length >= batchSize) {
|
||||
// Process the batch
|
||||
await processBatch(batch);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining chunks
|
||||
if (batch.length > 0) {
|
||||
await processBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
async function processBatch(chunks: string[]) {
|
||||
console.log(`Processing batch of ${chunks.length} chunks`);
|
||||
// Do something with the batch
|
||||
// e.g., save to database, send to queue, etc.
|
||||
}
|
||||
```
|
||||
|
||||
## Using with `runs.subscribeToRun()`
|
||||
|
||||
For more advanced use cases where you need both the run status and streams, you can use the `runs.subscribeToRun()` method with `.withStreams()`:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import type { myTask } from "./trigger/myTask";
|
||||
|
||||
async function subscribeToRunAndStreams(runId: string) {
|
||||
for await (const update of runs.subscribeToRun<typeof myTask>(runId).withStreams()) {
|
||||
switch (update.type) {
|
||||
case "run":
|
||||
console.log("Run update:", update.run.status);
|
||||
break;
|
||||
case "default":
|
||||
console.log("Stream chunk:", update.chunk);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
For most use cases, we recommend using `streams.read()` with defined streams for better type safety and clearer code. Use `runs.subscribeToRun().withStreams()` only when you need to track both run status and stream data simultaneously.
|
||||
</Note>
|
||||
@@ -0,0 +1,225 @@
|
||||
---
|
||||
title: "Run updates (backend)"
|
||||
sidebarTitle: "Run updates"
|
||||
description: "Subscribe to run status changes, metadata updates, and tag changes from your backend code using async iterators."
|
||||
---
|
||||
|
||||
**Subscribe to runs from your backend and get updates whenever status, metadata, or tags change.** Each function returns an async iterator that yields the run object on every change.
|
||||
|
||||
## runs.subscribeToRun
|
||||
|
||||
Subscribes to all changes to a specific run.
|
||||
|
||||
```ts Example
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
|
||||
for await (const run of runs.subscribeToRun("run_1234")) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
This function subscribes to all changes to a run. It returns an async iterator that yields the run object whenever the run is updated. The iterator will complete when the run is finished.
|
||||
|
||||
**Authentication**: This function supports both server-side and client-side authentication. For server-side authentication, use your API key. For client-side authentication, you must generate a public access token with read access to the specific run. See our [authentication guide](/realtime/auth) for details.
|
||||
|
||||
**Response**: The AsyncIterator yields the [run object](/realtime/run-object).
|
||||
|
||||
## runs.subscribeToRunsWithTag
|
||||
|
||||
Subscribes to all changes to runs with a specific tag.
|
||||
|
||||
```ts Example
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
|
||||
for await (const run of runs.subscribeToRunsWithTag("user:1234")) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
This function subscribes to all changes to runs with a specific tag. It returns an async iterator that yields the run object whenever a run with the specified tag is updated. This iterator will never complete, so you must manually break out of the loop when you no longer want to receive updates.
|
||||
|
||||
**Authentication**: This function supports both server-side and client-side authentication. For server-side authentication, use your API key. For client-side authentication, you must generate a public access token with read access to the specific tag. See our [authentication guide](/realtime/auth) for details.
|
||||
|
||||
**Response**: The AsyncIterator yields the [run object](/realtime/run-object).
|
||||
|
||||
## runs.subscribeToBatch
|
||||
|
||||
Subscribes to all changes for runs in a batch.
|
||||
|
||||
```ts Example
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
|
||||
for await (const run of runs.subscribeToBatch("batch_1234")) {
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
This function subscribes to all changes for runs in a batch. It returns an async iterator that yields a run object whenever a run in the batch is updated. The iterator does not complete on its own, you must manually `break` the loop when you want to stop listening for updates.
|
||||
|
||||
**Authentication**: This function supports both server-side and client-side authentication. For server-side authentication, use your API key. For client-side authentication, you must generate a public access token with read access to the specific batch. See our [authentication guide](/realtime/auth) for details.
|
||||
|
||||
**Response**: The AsyncIterator yields the [run object](/realtime/run-object).
|
||||
|
||||
## Type safety
|
||||
|
||||
You can infer the types of the run's payload and output by passing the type of the task to the subscribe functions:
|
||||
|
||||
```ts
|
||||
import { runs, tasks } from "@trigger.dev/sdk";
|
||||
import type { myTask } from "./trigger/my-task";
|
||||
|
||||
async function myBackend() {
|
||||
const handle = await tasks.trigger("my-task", { some: "data" });
|
||||
|
||||
for await (const run of runs.subscribeToRun<typeof myTask>(handle.id)) {
|
||||
// run.payload and run.output are now typed
|
||||
console.log(run.payload.some);
|
||||
|
||||
if (run.output) {
|
||||
console.log(run.output.some);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using `subscribeToRunsWithTag`, you can pass a union of task types:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import type { myTask, myOtherTask } from "./trigger/my-task";
|
||||
|
||||
for await (const run of runs.subscribeToRunsWithTag<typeof myTask | typeof myOtherTask>("my-tag")) {
|
||||
// Narrow down the type based on the taskIdentifier
|
||||
switch (run.taskIdentifier) {
|
||||
case "my-task": {
|
||||
console.log("Run output:", run.output.foo); // Type-safe
|
||||
break;
|
||||
}
|
||||
case "my-other-task": {
|
||||
console.log("Run output:", run.output.bar); // Type-safe
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Subscribe to metadata updates from your tasks
|
||||
|
||||
The metadata API allows you to update custom metadata on runs and receive real-time updates when metadata changes. This is useful for tracking progress, storing intermediate results, or adding custom status information that can be monitored in real-time.
|
||||
|
||||
<Note>
|
||||
For frontend applications using React, see our [React hooks metadata
|
||||
documentation](/realtime/react-hooks/subscribe#using-metadata-to-show-progress-in-your-ui) for
|
||||
consuming metadata updates in your UI.
|
||||
</Note>
|
||||
|
||||
When you update metadata from within a task using `metadata.set()`, `metadata.append()`, or other metadata methods, all subscribers to that run will automatically receive the updated run object containing the new metadata.
|
||||
|
||||
This makes metadata perfect for:
|
||||
|
||||
- Progress tracking
|
||||
- Status updates
|
||||
- Intermediate results
|
||||
- Custom notifications
|
||||
|
||||
Use the metadata API within your task to update metadata in real-time. In this basic example task, we're updating the progress of a task as it processes items.
|
||||
|
||||
### How to subscribe to metadata updates
|
||||
|
||||
This example task updates the progress of a task as it processes items.
|
||||
|
||||
```ts
|
||||
// Your task code
|
||||
import { task, metadata } from "@trigger.dev/sdk";
|
||||
|
||||
export const progressTask = task({
|
||||
id: "progress-task",
|
||||
run: async (payload: { items: string[] }) => {
|
||||
const total = payload.items.length;
|
||||
|
||||
for (let i = 0; i < payload.items.length; i++) {
|
||||
// Update progress metadata
|
||||
metadata.set("progress", {
|
||||
current: i + 1,
|
||||
total: total,
|
||||
percentage: Math.round(((i + 1) / total) * 100),
|
||||
currentItem: payload.items[i],
|
||||
});
|
||||
|
||||
// Process the item
|
||||
await processItem(payload.items[i]);
|
||||
}
|
||||
|
||||
metadata.set("status", "completed");
|
||||
return { processed: total };
|
||||
},
|
||||
});
|
||||
|
||||
async function processItem(item: string) {
|
||||
// Simulate work
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
```
|
||||
|
||||
We can now subscribe to the runs and receive real-time metadata updates.
|
||||
|
||||
```ts
|
||||
// Somewhere in your backend code
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import type { progressTask } from "./trigger/progress-task";
|
||||
|
||||
async function monitorProgress(runId: string) {
|
||||
for await (const run of runs.subscribeToRun<typeof progressTask>(runId)) {
|
||||
console.log(`Run ${run.id} status: ${run.status}`);
|
||||
|
||||
if (run.metadata?.progress) {
|
||||
const progress = run.metadata.progress as {
|
||||
current: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
currentItem: string;
|
||||
};
|
||||
|
||||
console.log(`Progress: ${progress.current}/${progress.total} (${progress.percentage}%)`);
|
||||
console.log(`Processing: ${progress.currentItem}`);
|
||||
}
|
||||
|
||||
if (run.metadata?.status === "completed") {
|
||||
console.log("Task completed!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For more information on how to write tasks that use the metadata API, as well as more examples, see our [run metadata docs](/runs/metadata#more-metadata-task-examples).
|
||||
|
||||
### Type safety
|
||||
|
||||
You can get type safety for your metadata by defining types:
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import type { progressTask } from "./trigger/progress-task";
|
||||
|
||||
interface ProgressMetadata {
|
||||
progress?: {
|
||||
current: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
currentItem: string;
|
||||
};
|
||||
status?: "running" | "completed" | "failed";
|
||||
}
|
||||
|
||||
async function monitorTypedProgress(runId: string) {
|
||||
for await (const run of runs.subscribeToRun<typeof progressTask>(runId)) {
|
||||
const metadata = run.metadata as ProgressMetadata;
|
||||
|
||||
if (metadata?.progress) {
|
||||
// Now you have full type safety
|
||||
console.log(`Progress: ${metadata.progress.percentage}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
title: "How Realtime works"
|
||||
sidebarTitle: "How it works"
|
||||
description: "Technical architecture behind Trigger.dev's real-time run updates, built on Electric SQL and PostgreSQL syncing."
|
||||
---
|
||||
|
||||
This page covers the infrastructure behind **run updates** (status, metadata, tags). [Streaming](/tasks/streams) uses a separate transport.
|
||||
|
||||
## Architecture
|
||||
|
||||
The run updates system is built on top of [Electric SQL](https://electric-sql.com/), an open-source PostgreSQL syncing engine. The Trigger.dev API wraps Electric SQL and provides a simple API to subscribe to [runs](/runs) and get updates as they happen.
|
||||
|
||||
## Run changes
|
||||
|
||||
You will receive updates whenever a run changes for the following reasons:
|
||||
|
||||
- The run moves to a new state. See our [run lifecycle docs](/runs#the-run-lifecycle) for more information.
|
||||
- [Run tags](/tags) are added or removed.
|
||||
- [Run metadata](/runs/metadata) is updated.
|
||||
|
||||
## Run object
|
||||
|
||||
The run object returned by Realtime subscriptions is optimized for streaming updates and differs from the management API's run object. See [the run object](/realtime/run-object) page for the complete schema and field descriptions.
|
||||
|
||||
## Basic usage
|
||||
|
||||
After you trigger a task, you can subscribe to the run using the `runs.subscribeToRun` function. This function returns an async iterator that you can use to get updates on the run status.
|
||||
|
||||
```ts
|
||||
import { runs, tasks } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
async function myBackend() {
|
||||
const handle = await tasks.trigger("my-task", { some: "data" });
|
||||
|
||||
for await (const run of runs.subscribeToRun(handle.id)) {
|
||||
// This will log the run every time it changes
|
||||
console.log(run);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every time the run changes, the async iterator will yield the updated run. You can use this to update your UI, log the run status, or take any other action.
|
||||
|
||||
Alternatively, you can subscribe to changes to any run that includes a specific tag (or tags) using the `runs.subscribeToRunsWithTag` function.
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
for await (const run of runs.subscribeToRunsWithTag("user:1234")) {
|
||||
// This will log the run every time it changes, for all runs with the tag "user:1234"
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
If you've used `batchTrigger` to trigger multiple runs, you can also subscribe to changes to all the runs triggered in the batch using the `runs.subscribeToBatch` function.
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your backend code
|
||||
for await (const run of runs.subscribeToBatch("batch-id")) {
|
||||
// This will log the run every time it changes, for all runs in the batch with the ID "batch-id"
|
||||
console.log(run);
|
||||
}
|
||||
```
|
||||
|
||||
## Run metadata
|
||||
|
||||
The run metadata API gives you the ability to add or update custom metadata on a run, which will cause the run to be updated. This allows you to extend the Realtime API with custom data attached to a run that can be used for various purposes. Some common use cases include:
|
||||
|
||||
- Adding a link to a related resource
|
||||
- Adding a reference to a user or organization
|
||||
- Adding a custom status with progress information
|
||||
|
||||
See our [run metadata docs](/runs/metadata) for more on how to write tasks that use the metadata API.
|
||||
|
||||
### Using metadata with Realtime & React hooks
|
||||
|
||||
You can combine run metadata with the Realtime API to bridge the gap between your trigger.dev tasks and your applications in two ways:
|
||||
|
||||
1. Using our [React hooks](/realtime/react-hooks/subscribe#using-metadata) to subscribe to metadata updates and update your UI in real-time.
|
||||
2. Using our [backend functions](/realtime/backend) to subscribe to metadata updates in your backend.
|
||||
|
||||
## Limits
|
||||
|
||||
The Realtime API in the Trigger.dev Cloud limits the number of concurrent subscriptions, depending on your plan. If you exceed the limit, you will receive an error when trying to subscribe to a run. For more information, see our [pricing page](https://trigger.dev/pricing).
|
||||
|
||||
## Learn more
|
||||
|
||||
- Read our Realtime blog post ["How we built a real-time service that handles 20,000 updates per second](https://trigger.dev/blog/how-we-built-realtime)
|
||||
- Using Realtime: [React Hooks (frontend)](/realtime/react-hooks)
|
||||
- Using [Backend (server-side)](/realtime/backend)
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: Realtime overview
|
||||
sidebarTitle: Overview
|
||||
description: "Get live run updates and stream data from background tasks to your frontend or backend. No polling."
|
||||
---
|
||||
|
||||
**Realtime is the umbrella for everything live in Trigger.dev.** It covers two things: getting notified when a run's state changes, and streaming continuous data (like AI tokens) from a running task to your app.
|
||||
|
||||
Both use the same `@trigger.dev/react-hooks` package and the same authentication system. The difference is what they give you.
|
||||
|
||||
## Run updates vs Streaming
|
||||
|
||||
| | Run updates | Streaming |
|
||||
|---|---|---|
|
||||
| **What you get** | Run state: status, metadata, tags | Continuous data you define (AI tokens, file chunks, progress) |
|
||||
| **When it fires** | On state changes | While the task runs, as data is produced |
|
||||
| **Use case** | Progress bars, status badges, dashboards | AI chat output, live logs, file processing |
|
||||
| **React hook** | [`useRealtimeRun`](/realtime/react-hooks/subscribe) | [`useRealtimeStream`](/realtime/react-hooks/streams) |
|
||||
| **Setup in task code?** | No, automatic | Yes, using `streams.define()` |
|
||||
| **Infrastructure** | [Electric SQL](/realtime/how-it-works) (PostgreSQL sync) | Streams transport |
|
||||
|
||||
You can use both at the same time. Subscribe to a run's status (to show a progress bar) while also streaming AI output (to display tokens as they arrive).
|
||||
|
||||
## Run updates
|
||||
|
||||
Subscribe to a run and your code gets called whenever its status, [metadata](/runs/metadata), or [tags](/tags) change. No setup needed in your task code.
|
||||
|
||||
You can subscribe to:
|
||||
|
||||
- **Specific runs** by run ID
|
||||
- **Runs with specific tags** (e.g., all runs tagged with `user:123`)
|
||||
- **Batch runs** within a specific batch
|
||||
- **Trigger + subscribe combos** that trigger a task and immediately subscribe (frontend only)
|
||||
|
||||
→ [React hooks](/realtime/react-hooks/subscribe) | [Backend](/realtime/backend/subscribe)
|
||||
|
||||
## Streaming
|
||||
|
||||
Define typed streams in your task, pipe data to them, and read that data from your frontend or backend as it's produced. You need to set up streams in your task code using `streams.define()`.
|
||||
|
||||
→ [How to emit streams from tasks](/tasks/streams) | [React hooks](/realtime/react-hooks/streams) | [Backend](/realtime/backend/streams)
|
||||
|
||||
## Authentication
|
||||
|
||||
All Realtime hooks and functions require authentication. See the [authentication guide](/realtime/auth) for setup.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### How do I show a progress bar for a background task?
|
||||
|
||||
Use [run metadata](/runs/metadata) to store progress data (like a percentage), then subscribe to the run with [`useRealtimeRun`](/realtime/react-hooks/subscribe). Your component re-renders on every metadata update.
|
||||
|
||||
### How do I stream AI/LLM responses from a background task?
|
||||
|
||||
Define a stream in your task with `streams.define()`, pipe your AI SDK response to it, then consume it in React with [`useRealtimeStream`](/realtime/react-hooks/streams). See [Streaming data from tasks](/tasks/streams) for the full guide.
|
||||
|
||||
### Do I need WebSockets or polling?
|
||||
|
||||
No. Run updates are powered by [Electric SQL](/realtime/how-it-works) (HTTP-based PostgreSQL syncing). Streams use their own transport. The hooks handle connections automatically.
|
||||
|
||||
### Can I use both run updates and streaming together?
|
||||
|
||||
Yes. A common pattern: subscribe to run status with `useRealtimeRun` (progress indicator) while streaming AI output with `useRealtimeStream` (token-by-token display).
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: "React hooks for real-time task updates"
|
||||
sidebarTitle: Overview
|
||||
description: "Subscribe to background task progress, stream AI responses, and trigger tasks from React components using @trigger.dev/react-hooks."
|
||||
---
|
||||
|
||||
import RealtimeExamplesCards from "/snippets/realtime-examples-cards.mdx";
|
||||
|
||||
**`@trigger.dev/react-hooks` gives your React components live access to background tasks.** Subscribe to run progress, stream AI output as it generates, or trigger tasks directly from the browser.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the `@trigger.dev/react-hooks` package in your project:
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
```bash npm
|
||||
npm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash pnpm
|
||||
pnpm add @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
```bash yarn
|
||||
yarn install @trigger.dev/react-hooks
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
All hooks require authentication with a Public Access Token. Pass the token via the `accessToken` option:
|
||||
|
||||
```tsx
|
||||
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://your-trigger-dev-instance.com", // optional, only needed if you are self-hosting Trigger.dev
|
||||
});
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Learn more about [generating and managing tokens in our authentication guide](/realtime/auth).
|
||||
|
||||
## Available hooks
|
||||
|
||||
| Hook category | What it does | Guide |
|
||||
|---|---|---|
|
||||
| **Trigger hooks** | Trigger tasks from the browser | [Triggering](/realtime/react-hooks/triggering) |
|
||||
| **Run updates** | Subscribe to run status, metadata, and tags | [Run updates](/realtime/react-hooks/subscribe) |
|
||||
| **Streaming** | Consume AI output, file chunks, or any continuous data | [Streaming](/realtime/react-hooks/streams) |
|
||||
| **SWR hooks** | One-time fetch with caching (not recommended for most cases) | [SWR](/realtime/react-hooks/swr) |
|
||||
|
||||
## SWR vs Realtime hooks
|
||||
|
||||
We offer two "styles" of hooks: SWR and Realtime. SWR hooks use the [swr](https://swr.vercel.app/) library to fetch data once and cache it. Realtime hooks use [Trigger.dev Realtime](/realtime) to subscribe to updates as they happen.
|
||||
|
||||
<Note>
|
||||
It can be a little confusing which one to use because [swr](https://swr.vercel.app/) can also be
|
||||
configured to poll for updates. But because of rate-limits and the way the Trigger.dev API works,
|
||||
we recommend using the Realtime hooks for most use cases.
|
||||
</Note>
|
||||
@@ -0,0 +1,449 @@
|
||||
---
|
||||
title: "Stream data to React (AI, files, progress)"
|
||||
sidebarTitle: "Streaming"
|
||||
description: "Display AI/LLM output token by token, stream file chunks, or pipe any continuous data from a background task into your React components."
|
||||
---
|
||||
|
||||
**Display AI responses as they generate, stream file processing results, or pipe any continuous data from a running task into your React components.** Unlike [progress and status hooks](/realtime/react-hooks/subscribe) (which track run state), streaming hooks give you the raw data your task produces while it runs.
|
||||
|
||||
<Note>
|
||||
To learn how to emit streams from your tasks, see [Streaming data from tasks](/tasks/streams).
|
||||
</Note>
|
||||
|
||||
## useRealtimeStream (Recommended)
|
||||
|
||||
<Note>
|
||||
Available in SDK version **4.1.0 or later**. This is the recommended way to consume streams in
|
||||
your React components.
|
||||
</Note>
|
||||
|
||||
The `useRealtimeStream` hook allows you to subscribe to a specific stream by its run ID and stream key. This hook is designed to work seamlessly with [defined streams](/tasks/streams#defining-typed-streams-recommended) for full type safety.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function StreamViewer({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { parts, error } = useRealtimeStream<string>(runId, "ai-output", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
if (!parts) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>{part}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Defined Streams
|
||||
|
||||
The recommended approach is to use defined streams for full type safety:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
import { aiStream } from "@/app/streams";
|
||||
|
||||
export function StreamViewer({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
// Pass the defined stream directly - full type safety!
|
||||
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
||||
accessToken: publicAccessToken,
|
||||
timeoutInSeconds: 600,
|
||||
onData: (chunk) => {
|
||||
console.log("New chunk:", chunk); // chunk is typed!
|
||||
},
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
if (!parts) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>{part}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming AI Responses
|
||||
|
||||
Here's a complete example showing how to display streaming AI responses:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
import { aiStream } from "@/trigger/streams";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
export function AIStreamViewer({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
||||
accessToken: publicAccessToken,
|
||||
timeoutInSeconds: 300,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
if (!parts) return <div>Loading stream...</div>;
|
||||
|
||||
const text = parts.join("");
|
||||
|
||||
return (
|
||||
<div className="prose">
|
||||
<Streamdown isAnimating={true}>{text}</Streamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
The `useRealtimeStream` hook accepts the following options:
|
||||
|
||||
```tsx
|
||||
const { parts, error } = useRealtimeStream(streamOrRunId, streamKeyOrOptions, {
|
||||
accessToken: "pk_...", // Required: Public access token
|
||||
baseURL: "https://api.trigger.dev", // Optional: Custom API URL
|
||||
timeoutInSeconds: 60, // Optional: Timeout (default: 60)
|
||||
startIndex: 0, // Optional: Start from specific chunk
|
||||
throttleInMs: 16, // Optional: Throttle updates (default: 16ms)
|
||||
onData: (chunk) => {}, // Optional: Callback for each chunk
|
||||
});
|
||||
```
|
||||
|
||||
### Using Default Stream
|
||||
|
||||
You can omit the stream key to use the default stream:
|
||||
|
||||
```tsx
|
||||
const { parts, error } = useRealtimeStream<string>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
```
|
||||
|
||||
For more information on defining and using streams, see the [Streaming data from tasks](/tasks/streams) documentation.
|
||||
|
||||
## useInputStreamSend
|
||||
|
||||
The `useInputStreamSend` hook lets you send data from your frontend into a running task's [input stream](/tasks/streams#input-streams). Use it for cancel buttons, approval forms, or any UI that needs to push typed data into a running task.
|
||||
|
||||
### Basic usage
|
||||
|
||||
Pass the input stream's `id` (string), the run ID, and options such as `accessToken`. You typically get `runId` and `accessToken` from the object returned when you trigger the task (e.g. `handle.id`, `handle.publicAccessToken`). The hook returns `send`, `isLoading`, `error`, and `isReady`:
|
||||
|
||||
```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 (
|
||||
<button
|
||||
disabled={!isReady || isLoading}
|
||||
onClick={() => send({ approved: true, reviewer: "alice" })}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
With a generic for type-safe payloads when not using a defined stream:
|
||||
|
||||
```tsx
|
||||
type ApprovalPayload = { approved: boolean; reviewer: string };
|
||||
const { send } = useInputStreamSend<ApprovalPayload>("approval", runId, {
|
||||
accessToken,
|
||||
});
|
||||
send({ approved: true, reviewer: "alice" });
|
||||
```
|
||||
|
||||
### Options and return value
|
||||
|
||||
- **`streamId`**: The input stream identifier (string). Use the `id` from your defined stream (e.g. `approval.id`) or the same string you used in `streams.input<T>({ id: "approval" })`.
|
||||
- **`runId`**: The run to send input to. When `runId` is undefined, `isReady` is false and `send` will not trigger.
|
||||
- **`options`**: `accessToken` (required for client usage), `baseURL` (optional). See [Realtime auth](/realtime/auth) for generating a public access token with the right scopes (e.g. input streams write for that run).
|
||||
|
||||
Return value:
|
||||
|
||||
- **`send(data)`**: Sends typed data to the input stream. Uses SWR mutation under the hood.
|
||||
- **`isLoading`**: True while a send is in progress.
|
||||
- **`error`**: Set if the last send failed.
|
||||
- **`isReady`**: True when both `runId` and access token are available.
|
||||
|
||||
For receiving input stream data inside a task (`.wait()`, `.once()`, `.on()`), see [Input Streams](/tasks/streams#input-streams) in the Streams doc.
|
||||
|
||||
## useRealtimeRunWithStreams
|
||||
|
||||
<Note>
|
||||
For new projects, we recommend using `useRealtimeStream` instead (available in SDK 4.1.0+). This
|
||||
hook is still supported for backward compatibility and use cases where you need to subscribe to
|
||||
both the run and all its streams at once.
|
||||
</Note>
|
||||
|
||||
The `useRealtimeRunWithStreams` hook allows you to subscribe to a run by its ID and also receive any streams that are emitted by the task. This is useful when you need to access both the run metadata and multiple streams simultaneously.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>
|
||||
{Object.keys(streams).map((stream) => (
|
||||
<div key={stream}>Stream: {stream}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can also provide the type of the streams to the `useRealtimeRunWithStreams` hook to get type-safety:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
As you can see above, each stream is an array of the type you provided, keyed by the stream name. If instead of a pure text stream you have a stream of objects, you can provide the type of the object:
|
||||
|
||||
```tsx
|
||||
import type { TextStreamPart } from "ai";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = { openai: TextStreamPart<{}> };
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams<typeof myTask, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
const text = streams.openai
|
||||
?.filter((stream) => stream.type === "text-delta")
|
||||
?.map((part) => part.text)
|
||||
.join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming AI responses with useRealtimeRunWithStreams
|
||||
|
||||
Here's an example showing how to display streaming OpenAI responses using `useRealtimeRunWithStreams`:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { aiStreaming, STREAMS } from "./trigger/ai-streaming";
|
||||
|
||||
function MyComponent({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) {
|
||||
const { streams } = useRealtimeRunWithStreams<typeof aiStreaming, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (!streams.openai) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const text = streams.openai.join(""); // `streams.openai` is an array of strings
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>OpenAI response:</h2>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### AI SDK with tools
|
||||
|
||||
When using the AI SDK with tools with `useRealtimeRunWithStreams`, you can access tool calls and results:
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { aiStreamingWithTools, STREAMS } from "./trigger/ai-streaming";
|
||||
|
||||
function MyComponent({ runId, publicAccessToken }: { runId: string; publicAccessToken: string }) {
|
||||
const { streams } = useRealtimeRunWithStreams<typeof aiStreamingWithTools, STREAMS>(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (!streams.openai) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
// streams.openai is an array of TextStreamPart
|
||||
const toolCall = streams.openai.find(
|
||||
(stream) => stream.type === "tool-call" && stream.toolName === "getWeather"
|
||||
);
|
||||
const toolResult = streams.openai.find((stream) => stream.type === "tool-result");
|
||||
const textDeltas = streams.openai.filter((stream) => stream.type === "text-delta");
|
||||
|
||||
const text = textDeltas.map((delta) => delta.textDelta).join("");
|
||||
const weatherLocation = toolCall ? toolCall.args.location : undefined;
|
||||
const weather = toolResult ? toolResult.result.temperature : undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>OpenAI response:</h2>
|
||||
<p>{text}</p>
|
||||
<h2>Weather:</h2>
|
||||
<p>
|
||||
{weatherLocation
|
||||
? `The weather in ${weatherLocation} is ${weather} degrees.`
|
||||
: "No weather data"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Throttling updates
|
||||
|
||||
The `useRealtimeRunWithStreams` hook accepts an `experimental_throttleInMs` option to throttle the updates from the server. This can be useful if you are getting too many updates and want to reduce the number of updates.
|
||||
|
||||
```tsx
|
||||
import { useRealtimeRunWithStreams } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({
|
||||
runId,
|
||||
publicAccessToken,
|
||||
}: {
|
||||
runId: string;
|
||||
publicAccessToken: string;
|
||||
}) {
|
||||
const { run, streams, error } = useRealtimeRunWithStreams(runId, {
|
||||
accessToken: publicAccessToken,
|
||||
experimental_throttleInMs: 1000, // Throttle updates to once per second
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run: {run.id}</div>
|
||||
{/* Display streams */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
All other options (accessToken, baseURL, enabled, id) work the same as the other realtime hooks.
|
||||
|
||||
For the newer `useRealtimeStream` hook, use the `throttleInMs` option instead (see [options above](#options)).
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
### How do I stream AI/LLM responses from a background task to React?
|
||||
|
||||
Define a typed stream in your task with `streams.define<string>()`, pipe your AI SDK response to it with `.pipe()`, then consume it in your component with `useRealtimeStream`. See [Streaming data from tasks](/tasks/streams) for the task-side setup.
|
||||
|
||||
### What's the difference between streaming and run updates?
|
||||
|
||||
[Run updates](/realtime/react-hooks/subscribe) track **run state** (status, metadata, tags). Streaming (this page) pipes **continuous data** your task produces. Use run updates for progress bars and status badges. Use streaming for AI chat output, live logs, or file processing results. You can use both at the same time.
|
||||
|
||||
### Can I send data back into a running task from React?
|
||||
|
||||
Yes. Use the `useInputStreamSend` hook to send data into a running task's input stream. This is useful for cancel buttons, user approvals, or any interactive flow. See [Input Streams](/tasks/streams#input-streams) for the full guide.
|
||||
|
||||
### Do streams work with the Vercel AI SDK?
|
||||
|
||||
Yes. You can pipe a Vercel AI SDK `streamText` response directly into a Trigger.dev stream using `.pipe()`. The [Streaming data from tasks](/tasks/streams) page has a complete AI streaming example.
|
||||
@@ -0,0 +1,674 @@
|
||||
---
|
||||
title: "Run updates in React"
|
||||
sidebarTitle: "Run updates"
|
||||
description: "Build progress bars, status indicators, and live dashboards by subscribing to background task updates from React components."
|
||||
---
|
||||
|
||||
**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](/realtime/react-hooks/streams) instead.
|
||||
|
||||
## Trigger + subscribe combo hooks
|
||||
|
||||
Trigger a task and immediately subscribe to its run. Details in the [triggering](/realtime/react-hooks/triggering) section.
|
||||
|
||||
- **[`useRealtimeTaskTrigger`](/realtime/react-hooks/triggering#userealtimetasktrigger)** - Trigger a task and subscribe to the run
|
||||
- **[`useRealtimeTaskTriggerWithStreams`](/realtime/react-hooks/triggering#userealtimetasktriggerwithstreams)** - Trigger a task and subscribe to both run updates and streams
|
||||
|
||||
## Subscribe hooks
|
||||
|
||||
### useRealtimeRun
|
||||
|
||||
The `useRealtimeRun` hook allows you to subscribe to a run by its ID.
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
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](/realtime/run-object) for the complete schema and [How it Works documentation](/realtime/how-it-works) for more technical details.
|
||||
|
||||
### useRealtimeRunsWithTag
|
||||
|
||||
The `useRealtimeRunsWithTag` hook allows you to subscribe to multiple runs with a specific tag.
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
"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](/realtime) 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.
|
||||
|
||||
<Note>To learn how to write tasks using metadata, see our [metadata](/runs/metadata) guide.</Note>
|
||||
|
||||
### Progress monitoring
|
||||
|
||||
This example demonstrates how to create a progress monitor component that can be used to display the progress of a run:
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
"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:
|
||||
|
||||
```tsx
|
||||
"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.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
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.
|
||||
|
||||
```tsx
|
||||
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](#using-metadata-to-show-progress-in-your-ui) 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](/realtime/react-hooks/streams) 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](/realtime/how-it-works)). Just pass a run ID and an access token.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: SWR hooks
|
||||
sidebarTitle: SWR
|
||||
description: Fetch and cache data using SWR-based hooks
|
||||
---
|
||||
|
||||
SWR hooks use the [swr](https://swr.vercel.app/) library to fetch data once and cache it. These hooks are useful when you need to fetch data without real-time updates.
|
||||
|
||||
<Note>
|
||||
While SWR can be configured to poll for updates, we recommend using our other [Realtime
|
||||
hooks](/realtime/react-hooks/) for most use-cases due to rate-limits and the way the Trigger.dev
|
||||
API works.
|
||||
</Note>
|
||||
|
||||
## useRun
|
||||
|
||||
The `useRun` hook allows you to fetch a run by its ID.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun(runId);
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
The `run` object returned is the same as the [run object](/management/runs/retrieve) returned by the Trigger.dev API. To correctly type the run's payload and output, you can provide the type of your task to the `useRun` hook:
|
||||
|
||||
```tsx
|
||||
import { useRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ runId }: { runId: string }) {
|
||||
const { run, error, isLoading } = useRun<typeof myTask>(runId, {
|
||||
refreshInterval: 0, // Disable polling
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
|
||||
// Now run.payload and run.output are correctly typed
|
||||
|
||||
return <div>Run: {run.id}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Common SWR options
|
||||
|
||||
You can pass the following options to the all SWR hooks:
|
||||
|
||||
<ParamField path="revalidateOnFocus" type="boolean">
|
||||
Revalidate the data when the window regains focus.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="revalidateOnReconnect" type="boolean">
|
||||
Revalidate the data when the browser regains a network connection.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="refreshInterval" type="number">
|
||||
Poll for updates at the specified interval (in milliseconds). Polling is not recommended for most
|
||||
use-cases. Use the Realtime hooks instead.
|
||||
</ParamField>
|
||||
|
||||
## Common SWR return values
|
||||
|
||||
<ResponseField name="error" type="Error">
|
||||
An error object if an error occurred while fetching the data.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isLoading" type="boolean">
|
||||
A boolean indicating if the data is currently being fetched.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isValidating" type="boolean">
|
||||
A boolean indicating if the data is currently being revalidated.
|
||||
</ResponseField>
|
||||
|
||||
<ResponseField name="isError" type="boolean">
|
||||
A boolean indicating if an error occurred while fetching the data.
|
||||
</ResponseField>{" "}
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
title: "Trigger tasks from React"
|
||||
sidebarTitle: Triggering
|
||||
description: "Trigger background tasks from React components and optionally subscribe to their progress or stream their output."
|
||||
---
|
||||
|
||||
Trigger tasks directly from your React components. You can fire-and-forget, or trigger and immediately subscribe to the run's progress or streamed output.
|
||||
|
||||
<Note>
|
||||
For triggering tasks from your frontend, you need to use “trigger” tokens. These can only be used
|
||||
once to trigger a task and are more secure than regular Public Access Tokens. To learn more about
|
||||
how to create and use these tokens, see our [Trigger
|
||||
Tokens](/realtime/auth#trigger-tokens-for-frontend-triggering-only) documentation.
|
||||
</Note>
|
||||
|
||||
## Hooks
|
||||
|
||||
We provide three hooks for triggering tasks from your frontend application:
|
||||
|
||||
- `useTaskTrigger` - Trigger a task from your frontend application.
|
||||
- `useRealtimeTaskTrigger` - Trigger a task from your frontend application and subscribe to the run.
|
||||
- `useRealtimeTaskTriggerWithStreams` - Trigger a task from your frontend application and subscribe to the run, and also receive any streams that are emitted by the task.
|
||||
|
||||
### useTaskTrigger
|
||||
|
||||
The `useTaskTrigger` hook allows you to trigger a task from your frontend application.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
// 👆 This is the type of your task, include this to get type-safety
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
// pass the type of your task here 👇
|
||||
const { submit, handle, error, isLoading } = useTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken, // 👈 this is the "trigger" token
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
return <div>Run ID: {handle.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`useTaskTrigger` returns an object with the following properties:
|
||||
|
||||
- `submit`: A function that triggers the task. It takes the payload of the task as an argument.
|
||||
- `handle`: The run handle object. This object contains the ID of the run that was triggered, along with a Public Access Token that can be used to access the run.
|
||||
- `isLoading`: A boolean that indicates whether the task is currently being triggered.
|
||||
- `error`: An error object that contains any errors that occurred while triggering the task.
|
||||
|
||||
The `submit` function triggers the task with the specified payload. You can additionally pass an optional [options](/triggering#options) argument to the `submit` function:
|
||||
|
||||
```tsx
|
||||
submit({ foo: "bar" }, { tags: ["tag1", "tag2"] });
|
||||
```
|
||||
|
||||
#### Using the handle object
|
||||
|
||||
You can use the `handle` object to initiate a subsequent [Realtime hook](/realtime/react-hooks/subscribe#userealtimerun) to subscribe to the run.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useTaskTrigger, useRealtimeRun } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
// 👆 This is the type of your task
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
// pass the type of your task here 👇
|
||||
const { submit, handle, error, isLoading } = useTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken, // 👈 this is the "trigger" token
|
||||
});
|
||||
|
||||
// use the handle object to preserve type-safety 👇
|
||||
const { run, error: realtimeError } = useRealtimeRun(handle, {
|
||||
accessToken: handle?.publicAccessToken,
|
||||
enabled: !!handle, // Only subscribe to the run if the handle is available
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
return <div>Run ID: {handle.id}</div>;
|
||||
}
|
||||
|
||||
if (realtimeError) {
|
||||
return <div>Error: {realtimeError.message}</div>;
|
||||
}
|
||||
|
||||
if (run) {
|
||||
return <div>Run ID: {run.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
We've also created some additional hooks that allow you to trigger tasks and subscribe to the run in one step:
|
||||
|
||||
### useRealtimeTaskTrigger
|
||||
|
||||
The `useRealtimeTaskTrigger` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime:
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTrigger } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, error, isLoading } = useRealtimeTaskTrigger<typeof myTask>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
// This is the Realtime run object, which will automatically update when the run changes
|
||||
if (run) {
|
||||
return <div>Run ID: {run.id}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### useRealtimeTaskTriggerWithStreams
|
||||
|
||||
The `useRealtimeTaskTriggerWithStreams` hook allows you to trigger a task from your frontend application and then subscribe to the run in using Realtime, and also receive any streams that are emitted by the task.
|
||||
|
||||
```tsx
|
||||
"use client"; // This is needed for Next.js App Router or other RSC frameworks
|
||||
|
||||
import { useRealtimeTaskTriggerWithStreams } from "@trigger.dev/react-hooks";
|
||||
import type { myTask } from "@/trigger/myTask";
|
||||
|
||||
type STREAMS = {
|
||||
openai: string; // this is the type of each "part" of the stream
|
||||
};
|
||||
|
||||
export function MyComponent({ publicAccessToken }: { publicAccessToken: string }) {
|
||||
const { submit, run, streams, error, isLoading } = useRealtimeTaskTriggerWithStreams<
|
||||
typeof myTask,
|
||||
STREAMS
|
||||
>("my-task", {
|
||||
accessToken: publicAccessToken,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
|
||||
if (streams && run) {
|
||||
const text = streams.openai?.map((part) => part).join("");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Run ID: {run.id}</div>
|
||||
<div>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={() => submit({ foo: "bar" })} disabled={isLoading}>
|
||||
{isLoading ? "Loading..." : "Trigger Task"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: useWaitToken
|
||||
description: Use the useWaitToken hook to complete a wait token from a React component
|
||||
---
|
||||
|
||||
We've added a new `useWaitToken` react hook that allows you to complete a wait token from a React component, using a Public Access Token.
|
||||
|
||||
```ts backend.ts
|
||||
import { wait } from "@trigger.dev/sdk";
|
||||
|
||||
// Somewhere in your code, you'll need to create the token and then pass the token ID and the public token to the frontend
|
||||
const token = await wait.createToken({
|
||||
timeout: "10m",
|
||||
});
|
||||
|
||||
return {
|
||||
tokenId: token.id,
|
||||
publicToken: token.publicAccessToken, // An automatically generated public access token that expires in 1 hour
|
||||
};
|
||||
```
|
||||
|
||||
Now you can use the `useWaitToken` hook in your frontend code:
|
||||
|
||||
```tsx frontend.tsx
|
||||
import { useWaitToken } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function MyComponent({ publicToken, tokenId }: { publicToken: string; tokenId: string }) {
|
||||
const { complete } = useWaitToken(tokenId, {
|
||||
accessToken: publicToken,
|
||||
});
|
||||
|
||||
return <button onClick={() => complete({ foo: "bar" })}>Complete</button>;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: "The run object"
|
||||
sidebarTitle: "The run object"
|
||||
description: "The run object schema for Realtime subscriptions"
|
||||
---
|
||||
|
||||
The [run object](/realtime/run-object#the-run-object) is the main object returned by Realtime subscriptions (e.g., `runs.subscribeToRun()`). It contains all the information about the run, including the run ID, task identifier, payload, output, and more.
|
||||
|
||||
Type-safety is supported for the run object, so you can infer the types of the run's payload and output. See [type-safety](#type-safety) for more information.
|
||||
|
||||
## The run object
|
||||
|
||||
### Properties
|
||||
|
||||
<ParamField path="id" type="string" required>
|
||||
The run ID.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="taskIdentifier" type="string" required>
|
||||
The task identifier.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="payload" type="object" required>
|
||||
The input payload for the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="output" type="object">
|
||||
The output result of the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="createdAt" type="Date" required>
|
||||
Timestamp when the run was created.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="updatedAt" type="Date" required>
|
||||
Timestamp when the run was last updated.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="number" type="number" required>
|
||||
Sequential number assigned to the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="status" type="RunStatus" required>
|
||||
Current status of the run.
|
||||
|
||||
<Accordion title="RunStatus enum">
|
||||
|
||||
| Status | Description |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `WAITING_FOR_DEPLOY` | Task hasn't been deployed yet but is waiting to be executed |
|
||||
| `QUEUED` | Run is waiting to be executed by a worker |
|
||||
| `EXECUTING` | Run is currently being executed by a worker |
|
||||
| `REATTEMPTING` | Run has failed and is waiting to be retried |
|
||||
| `FROZEN` | Run has been paused by the system, and will be resumed by the system |
|
||||
| `COMPLETED` | Run has been completed successfully |
|
||||
| `CANCELED` | Run has been canceled by the user |
|
||||
| `FAILED` | Run has been completed with errors |
|
||||
| `CRASHED` | Run has crashed and won't be retried, most likely the worker ran out of resources, e.g. memory or storage |
|
||||
| `INTERRUPTED` | Run was interrupted during execution, mostly this happens in development environments |
|
||||
| `SYSTEM_FAILURE` | Run has failed to complete, due to an error in the system |
|
||||
| `DELAYED` | Run has been scheduled to run at a specific time |
|
||||
| `EXPIRED` | Run has expired and won't be executed |
|
||||
| `TIMED_OUT` | Run has reached it's maxDuration and has been stopped |
|
||||
|
||||
</Accordion>
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="durationMs" type="number" required>
|
||||
Duration of the run in milliseconds.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="costInCents" type="number" required>
|
||||
Total cost of the run in cents.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="baseCostInCents" type="number" required>
|
||||
Base cost of the run in cents before any additional charges.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="tags" type="string[]" required>
|
||||
Array of tags associated with the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="idempotencyKey" type="string">
|
||||
Key used to ensure idempotent execution.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="expiredAt" type="Date">
|
||||
Timestamp when the run expired.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="ttl" type="string">
|
||||
Time-to-live duration for the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="finishedAt" type="Date">
|
||||
Timestamp when the run finished.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="startedAt" type="Date">
|
||||
Timestamp when the run started.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="delayedUntil" type="Date">
|
||||
Timestamp until which the run is delayed.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="queuedAt" type="Date">
|
||||
Timestamp when the run was queued.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="metadata" type="Record<string, DeserializedJson>">
|
||||
Additional metadata associated with the run.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="error" type="SerializedError">
|
||||
Error information if the run failed.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="isTest" type="boolean" required>
|
||||
Indicates whether this is a test run.
|
||||
</ParamField>
|
||||
|
||||
## Type-safety
|
||||
|
||||
You can infer the types of the run's payload and output by passing the type of the task to the `subscribeToRun` function. This will give you type-safe access to the run's payload and output.
|
||||
|
||||
```ts
|
||||
import { runs, tasks } from "@trigger.dev/sdk";
|
||||
import type { myTask } from "./trigger/my-task";
|
||||
|
||||
// Somewhere in your backend code
|
||||
async function myBackend() {
|
||||
const handle = await tasks.trigger("my-task", { some: "data" });
|
||||
|
||||
for await (const run of runs.subscribeToRun<typeof myTask>(handle.id)) {
|
||||
// This will log the run every time it changes
|
||||
console.log(run.payload.some);
|
||||
|
||||
if (run.output) {
|
||||
// This will log the output if it exists
|
||||
console.log(run.output.some);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When using `subscribeToRunsWithTag`, you can pass a union of task types for all the possible tasks that can have the tag.
|
||||
|
||||
```ts
|
||||
import { runs } from "@trigger.dev/sdk";
|
||||
import type { myTask, myOtherTask } from "./trigger/my-task";
|
||||
|
||||
// Somewhere in your backend code
|
||||
for await (const run of runs.subscribeToRunsWithTag<typeof myTask | typeof myOtherTask>("my-tag")) {
|
||||
// You can narrow down the type based on the taskIdentifier
|
||||
switch (run.taskIdentifier) {
|
||||
case "my-task": {
|
||||
console.log("Run output:", run.output.foo); // This will be type-safe
|
||||
break;
|
||||
}
|
||||
case "my-other-task": {
|
||||
console.log("Run output:", run.output.bar); // This will be type-safe
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This works with all realtime subscription functions:
|
||||
|
||||
- `runs.subscribeToRun<TaskType>()`
|
||||
- `runs.subscribeToRunsWithTag<TaskType>()`
|
||||
- `runs.subscribeToBatch<TaskType>()`
|
||||
Reference in New Issue
Block a user