chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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}%`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user