chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
---
|
||||
title: "Tasks: Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Tasks are functions that can run for a long time and provide strong resilience to failure."
|
||||
---
|
||||
|
||||
There are different types of tasks including regular tasks and [scheduled tasks](/tasks/scheduled).
|
||||
|
||||
## Hello world task and how to trigger it
|
||||
|
||||
Here's an incredibly simple task:
|
||||
|
||||
```ts /trigger/hello-world.ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
|
||||
const helloWorld = task({
|
||||
//1. Use a unique id for each task
|
||||
id: "hello-world",
|
||||
//2. The run function is the main function of the task
|
||||
run: async (payload: { message: string }) => {
|
||||
//3. You can write code that runs for a long time here, there are no timeouts
|
||||
console.log(payload.message);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can trigger this in two ways:
|
||||
|
||||
1. From the dashboard [using the "Test" feature](/run-tests).
|
||||
2. Trigger it from your backend code. See the [full triggering guide here](/triggering).
|
||||
|
||||
Here's how to trigger a single run from elsewhere in your code:
|
||||
|
||||
```ts Your backend code
|
||||
import { helloWorld } from "./trigger/hello-world";
|
||||
|
||||
async function triggerHelloWorld() {
|
||||
//This triggers the task and returns a handle
|
||||
const handle = await helloWorld.trigger({ message: "Hello world!" });
|
||||
|
||||
//You can use the handle to check the status of the task, cancel and retry it.
|
||||
console.log("Task is running with handle", handle.id);
|
||||
}
|
||||
```
|
||||
|
||||
You can also [trigger a task from another task](/triggering), and wait for the result.
|
||||
|
||||
## Defining a `task`
|
||||
|
||||
The task function takes an object with the following fields.
|
||||
|
||||
### The `id` field
|
||||
|
||||
This is used to identify your task so it can be triggered, managed, and you can view runs in the dashboard. This must be unique in your project – we recommend making it descriptive and unique.
|
||||
|
||||
### The `run` function
|
||||
|
||||
Your custom code inside `run()` will be executed when your task is triggered. It’s an async function that has two arguments:
|
||||
|
||||
1. The run payload - the data that you pass to the task when you trigger it.
|
||||
2. An object with `ctx` about the run (Context), and any output from the optional `init` function that runs before every run attempt.
|
||||
|
||||
Anything you return from the `run` function will be the result of the task. Data you return must be JSON serializable: strings, numbers, booleans, arrays, objects, and null.
|
||||
|
||||
### `retry` options
|
||||
|
||||
A task is retried if an error is thrown. By default, we retry 3 times.
|
||||
|
||||
You can set the number of retries and the delay between retries in the `retry` field:
|
||||
|
||||
```ts /trigger/retry.ts
|
||||
export const taskWithRetries = task({
|
||||
id: "task-with-retries",
|
||||
retry: {
|
||||
maxAttempts: 10,
|
||||
factor: 1.8,
|
||||
minTimeoutInMs: 500,
|
||||
maxTimeoutInMs: 30_000,
|
||||
randomize: false,
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
| Option | What it does |
|
||||
|---|---|
|
||||
| `maxAttempts` | Total number of attempts (including the first). Default: 3 |
|
||||
| `factor` | Exponential backoff multiplier. Each retry delay = previous delay x factor. With `factor: 1.8` and `minTimeoutInMs: 500`, retries wait 500ms, 900ms, 1620ms, etc. |
|
||||
| `minTimeoutInMs` | Delay before the first retry |
|
||||
| `maxTimeoutInMs` | Cap on the delay between retries |
|
||||
| `randomize` | Add jitter to retry delays to prevent multiple failing tasks from retrying in lockstep |
|
||||
|
||||
<Note>Task-level retry settings override the defaults in your `trigger.config` file.</Note>
|
||||
|
||||
For more information read [the retrying guide](/errors-retrying).
|
||||
|
||||
It's also worth mentioning that you can [retry a block of code](/errors-retrying) inside your tasks as well.
|
||||
|
||||
### `queue` options
|
||||
|
||||
Queues allow you to control the concurrency of your tasks. This allows you to have one-at-a-time execution and parallel executions. There are also more advanced techniques like having different concurrencies for different sets of your users. For more information read [the concurrency & queues guide](/queue-concurrency).
|
||||
|
||||
```ts /trigger/one-at-a-time.ts
|
||||
export const oneAtATime = task({
|
||||
id: "one-at-a-time",
|
||||
queue: {
|
||||
concurrencyLimit: 1,
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `machine` options
|
||||
|
||||
Some tasks require more vCPUs or GBs of RAM. You can specify these requirements in the `machine` field. For more information read [the machines guide](/machines).
|
||||
|
||||
```ts /trigger/heavy-task.ts
|
||||
export const heavyTask = task({
|
||||
id: "heavy-task",
|
||||
machine: {
|
||||
preset: "large-1x", // 4 vCPU, 8 GB RAM
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `maxDuration` option
|
||||
|
||||
By default tasks can execute indefinitely, which can be great! But you also might want to set a `maxDuration` to prevent a task from running too long. You can set the `maxDuration` on a task, and all runs of that task will be stopped if they exceed the duration.
|
||||
|
||||
```ts /trigger/long-task.ts
|
||||
export const longTask = task({
|
||||
id: "long-task",
|
||||
maxDuration: 300, // 300 seconds or 5 minutes
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See our [maxDuration guide](/runs/max-duration) for more information.
|
||||
|
||||
### `ttl` option
|
||||
|
||||
You can set a default time-to-live (TTL) on a task. If a run is not dequeued within this time, it will expire and never execute. This is useful for time-sensitive tasks where stale runs should be discarded.
|
||||
|
||||
```ts /trigger/time-sensitive-task.ts
|
||||
export const timeSensitiveTask = task({
|
||||
id: "time-sensitive-task",
|
||||
ttl: "10m", // Also accepts a number of seconds, e.g. 600
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can override this per-trigger by passing `ttl` in the trigger options, or set a project-wide default in your [trigger.config.ts](/config/config-file#ttl). Set `ttl: 0` to opt out of a config-level TTL. See [Time-to-live (TTL)](/runs#time-to-live-ttl) for more information.
|
||||
|
||||
## Global lifecycle hooks
|
||||
|
||||
<Note>When specifying global lifecycle hooks, we recommend using the `init.ts` file.</Note>
|
||||
|
||||
You can register global lifecycle hooks that are executed for all runs, regardless of the task. While you can still define these in the `trigger.config.ts` file, you can also register them anywhere in your codebase:
|
||||
|
||||
```typescript
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onStartAttempt(({ ctx, payload, task }) => {
|
||||
console.log("Run started", ctx.run);
|
||||
});
|
||||
|
||||
tasks.onSuccess(({ ctx, output }) => {
|
||||
console.log("Run finished", ctx.run);
|
||||
});
|
||||
|
||||
tasks.onFailure(({ ctx, error }) => {
|
||||
console.log("Run failed", ctx.run);
|
||||
});
|
||||
```
|
||||
|
||||
### `init.ts`
|
||||
|
||||
If you create an `init.ts` file at the root of your trigger directory, it will be automatically loaded when a task is executed. This is useful for registering global lifecycle hooks, initializing a database connection, etc.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onStartAttempt(({ ctx, payload, task }) => {
|
||||
console.log("Run started", ctx.run);
|
||||
});
|
||||
```
|
||||
|
||||
## Lifecycle functions
|
||||
|
||||

|
||||
|
||||
### `middleware` and `locals` functions
|
||||
|
||||
Our task middleware system runs at the top level, executing before and after all lifecycle hooks. This allows you to wrap the entire task execution lifecycle with custom logic.
|
||||
|
||||
<Info>
|
||||
An error thrown in `middleware` is just like an uncaught error in the run function: it will
|
||||
propagate through to `catchError()` function and then will fail the attempt (either causing a
|
||||
retry or failing the run).
|
||||
</Info>
|
||||
|
||||
The `locals` API allows you to share data between middleware and hooks.
|
||||
|
||||
```ts db.ts
|
||||
import { locals } from "@trigger.dev/sdk";
|
||||
import { logger, tasks } from "@trigger.dev/sdk";
|
||||
|
||||
// This would be type of your database client here
|
||||
const DbLocal = locals.create<{ connect: () => Promise<void>; disconnect: () => Promise<void> }>(
|
||||
"db"
|
||||
);
|
||||
|
||||
export function getDb() {
|
||||
return locals.getOrThrow(DbLocal);
|
||||
}
|
||||
|
||||
export function setDb(db: { connect: () => Promise<void> }) {
|
||||
locals.set(DbLocal, db);
|
||||
}
|
||||
|
||||
tasks.middleware("db", async ({ ctx, payload, next, task }) => {
|
||||
// This would be your database client here
|
||||
const db = locals.set(DbLocal, {
|
||||
connect: async () => {
|
||||
logger.info("Connecting to the database");
|
||||
},
|
||||
disconnect: async () => {
|
||||
logger.info("Disconnecting from the database");
|
||||
},
|
||||
});
|
||||
|
||||
await db.connect();
|
||||
|
||||
await next();
|
||||
|
||||
await db.disconnect();
|
||||
});
|
||||
|
||||
// Disconnect when the run is paused
|
||||
tasks.onWait("db", async ({ ctx, payload, task }) => {
|
||||
const db = getDb();
|
||||
await db.disconnect();
|
||||
});
|
||||
|
||||
// Reconnect when the run is resumed
|
||||
tasks.onResume("db", async ({ ctx, payload, task }) => {
|
||||
const db = getDb();
|
||||
await db.connect();
|
||||
});
|
||||
```
|
||||
|
||||
You can access the database client using `getDb()` in your tasks `run` function and all your hooks (global or task specific):
|
||||
|
||||
```typescript
|
||||
import { getDb } from "./db";
|
||||
|
||||
export const myTask = task({
|
||||
run: async (payload: any, { ctx }) => {
|
||||
const db = getDb();
|
||||
await db.query("SELECT 1");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Per-task middleware
|
||||
|
||||
You can also define middleware per task by passing the `middleware` option in the task definition. This runs after global middleware and before the `run` function. Use it when only specific tasks need certain locals or setup:
|
||||
|
||||
```typescript
|
||||
import { task, locals } from "@trigger.dev/sdk";
|
||||
|
||||
const myLocal = locals.create<string>("myLocal");
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
middleware: async ({ payload, ctx, next }) => {
|
||||
locals.set(myLocal, "some-value");
|
||||
await next();
|
||||
},
|
||||
run: async (payload) => {
|
||||
const value = locals.getOrThrow(myLocal);
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `onStartAttempt` function
|
||||
|
||||
<Info>The `onStartAttempt` function was introduced in v4.1.0</Info>
|
||||
|
||||
Before a task run attempt starts, the `onStartAttempt` function is called. It's useful for sending notifications, logging, and other side effects.
|
||||
|
||||
```ts /trigger/on-start.ts
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onStartAttempt` function using `tasks.onStartAttempt()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onStartAttempt(({ ctx, payload, task }) => {
|
||||
console.log(
|
||||
`Run ${ctx.run.id} started on task ${task} attempt ${ctx.run.attempt.number}`,
|
||||
ctx.run
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onStartAttempt` function will cause the attempt to fail.</Info>
|
||||
|
||||
If you want to execute code before just the first attempt, you can use the `onStartAttempt` function and check `ctx.run.attempt.number === 1`:
|
||||
|
||||
```ts /trigger/on-start-attempt.ts
|
||||
export const taskWithOnStartAttempt = task({
|
||||
id: "task-with-on-start-attempt",
|
||||
onStartAttempt: async ({ payload, ctx }) => {
|
||||
if (ctx.run.attempt.number === 1) {
|
||||
console.log("Run started on attempt 1", ctx.run);
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### `onWait` and `onResume` functions
|
||||
|
||||
These lifecycle hooks allow you to run code when a run is paused or resumed because of a wait:
|
||||
|
||||
```typescript
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
onWait: async ({ wait }) => {
|
||||
console.log("Run paused", wait);
|
||||
},
|
||||
onResume: async ({ wait }) => {
|
||||
console.log("Run resumed", wait);
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
console.log("Run started", ctx.run);
|
||||
|
||||
await wait.for({ seconds: 10 });
|
||||
|
||||
console.log("Run finished", ctx.run);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define global `onWait` and `onResume` functions using `tasks.onWait()` and `tasks.onResume()`:
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onWait(({ ctx, payload, wait, task }) => {
|
||||
console.log("Run paused", ctx.run, wait);
|
||||
});
|
||||
|
||||
tasks.onResume(({ ctx, payload, wait, task }) => {
|
||||
console.log("Run resumed", ctx.run, wait);
|
||||
});
|
||||
```
|
||||
|
||||
### `onSuccess` function
|
||||
|
||||
When a task run succeeds, the `onSuccess` function is called. It's useful for sending notifications, logging, syncing state to your database, or other side effects.
|
||||
|
||||
```ts /trigger/on-success.ts
|
||||
export const taskWithOnSuccess = task({
|
||||
id: "task-with-on-success",
|
||||
onSuccess: async ({ payload, output, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onSuccess` function using `tasks.onSuccess()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onSuccess(({ ctx, payload, output }) => {
|
||||
console.log("Task succeeded", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Errors thrown in the `onSuccess` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
### `onComplete` function
|
||||
|
||||
This hook is executed when a run completes, regardless of whether it succeeded or failed:
|
||||
|
||||
```ts /trigger/on-complete.ts
|
||||
export const taskWithOnComplete = task({
|
||||
id: "task-with-on-complete",
|
||||
onComplete: async ({ payload, output, ctx }) => {
|
||||
if (result.ok) {
|
||||
console.log("Run succeeded", result.data);
|
||||
} else {
|
||||
console.log("Run failed", result.error);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onComplete` function using `tasks.onComplete()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onComplete(({ ctx, payload, output }) => {
|
||||
console.log("Task completed", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Errors thrown in the `onComplete` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
### `onFailure` function
|
||||
|
||||
When a task run fails, the `onFailure` function is called. It's useful for sending notifications, logging, or other side effects. It will only be executed once the task run has exhausted all its retries.
|
||||
|
||||
```ts /trigger/on-failure.ts
|
||||
export const taskWithOnFailure = task({
|
||||
id: "task-with-on-failure",
|
||||
onFailure: async ({ payload, error, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onFailure` function using `tasks.onFailure()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onFailure(({ ctx, payload, error }) => {
|
||||
console.log("Task failed", ctx.task.id);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Errors thrown in the `onFailure` function will be ignored, but you will still be able to see them
|
||||
in the dashboard.
|
||||
</Info>
|
||||
|
||||
<Note>
|
||||
`onFailure` doesn’t fire for some of the run statuses like `Crashed`, `System failures`, and
|
||||
`Canceled`.
|
||||
</Note>
|
||||
|
||||
### `catchError` functions
|
||||
|
||||
You can define a function that will be called when an error is thrown in the `run` function, that allows you to control how the error is handled and whether the task should be retried.
|
||||
|
||||
Read more about `catchError` in our [Errors and Retrying guide](/errors-retrying).
|
||||
|
||||
<Info>Uncaught errors will throw a special internal error of the type `HANDLE_ERROR_ERROR`.</Info>
|
||||
|
||||
### `onCancel` function
|
||||
|
||||
You can define an `onCancel` hook that is called when a run is cancelled. This is useful if you want to clean up any resources that were allocated for the run.
|
||||
|
||||
```typescript
|
||||
tasks.onCancel(({ ctx, signal }) => {
|
||||
console.log("Run cancelled", signal);
|
||||
});
|
||||
```
|
||||
|
||||
You can use the `onCancel` hook along with the `signal` passed into the run function to interrupt a call to an external service, for example using the [streamText](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) function from the AI SDK:
|
||||
|
||||
```typescript
|
||||
import { logger, tasks, schemaTask } from "@trigger.dev/sdk";
|
||||
import { streamText } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
export const interruptibleChat = schemaTask({
|
||||
id: "interruptible-chat",
|
||||
description: "Chat with the AI",
|
||||
schema: z.object({
|
||||
prompt: z.string().describe("The prompt to chat with the AI"),
|
||||
}),
|
||||
run: async ({ prompt }, { signal }) => {
|
||||
const chunks: TextStreamPart<{}>[] = [];
|
||||
|
||||
// 👇 This is a global onCancel hook, but it's inside of the run function
|
||||
tasks.onCancel(async () => {
|
||||
// We have access to the chunks here, and can save them to the database
|
||||
await saveChunksToDatabase(chunks);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = streamText({
|
||||
model: getModel(),
|
||||
prompt,
|
||||
experimental_telemetry: {
|
||||
isEnabled: true,
|
||||
},
|
||||
tools: {},
|
||||
abortSignal: signal, // 👈 Pass the signal to the streamText function, which aborts with the run is cancelled
|
||||
onChunk: ({ chunk }) => {
|
||||
chunks.push(chunk);
|
||||
},
|
||||
});
|
||||
|
||||
const textParts = [];
|
||||
|
||||
for await (const part of result.textStream) {
|
||||
textParts.push(part);
|
||||
}
|
||||
|
||||
return textParts.join("");
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
// streamText will throw an AbortError if the signal is aborted, so we can handle it here
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The `onCancel` hook can optionally wait for the `run` function to finish, and access the output of the run:
|
||||
|
||||
```typescript
|
||||
import { logger, task } from "@trigger.dev/sdk";
|
||||
import { setTimeout } from "node:timers/promises";
|
||||
|
||||
export const cancelExampleTask = task({
|
||||
id: "cancel-example",
|
||||
// Signal will be aborted when the task is cancelled 👇
|
||||
run: async (payload: { message: string }, { signal }) => {
|
||||
try {
|
||||
// We pass the signal to setTimeout to abort the timeout if the task is cancelled
|
||||
await setTimeout(10_000, undefined, { signal });
|
||||
} catch (error) {
|
||||
// Ignore the abort error
|
||||
}
|
||||
|
||||
// Do some more work here
|
||||
|
||||
return {
|
||||
message: "Hello, world!",
|
||||
};
|
||||
},
|
||||
onCancel: async ({ runPromise }) => {
|
||||
// You can await the runPromise to get the output of the task
|
||||
const output = await runPromise;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
You will have up to 30 seconds to complete the `runPromise` in the `onCancel` hook. After that
|
||||
point the process will be killed.
|
||||
</Note>
|
||||
|
||||
<Warning>
|
||||
`onCancel` only runs if the run is actively executing. If a run is cancelled while queued or
|
||||
suspended (e.g. waiting for a token), no machine is spun up and `onCancel` will not be called.
|
||||
This is a known limitation we're planning to address. Follow the progress on our [feedback
|
||||
board](https://feedback.trigger.dev/p/call-the-onfailure-hook-for-runs-that-were-canceled-expired).
|
||||
</Warning>
|
||||
|
||||
### `onStart` function (deprecated)
|
||||
|
||||
<Info>The `onStart` function was deprecated in v4.1.0. Use `onStartAttempt` instead.</Info>
|
||||
|
||||
When a task run starts, the `onStart` function is called. It's useful for sending notifications, logging, and other side effects.
|
||||
|
||||
<Warning>
|
||||
This function will only be called once per run (not per attempt). If you want to run code before
|
||||
each attempt, use a middleware function or the `onStartAttempt` function.
|
||||
</Warning>
|
||||
|
||||
```ts /trigger/on-start.ts
|
||||
export const taskWithOnStart = task({
|
||||
id: "task-with-on-start",
|
||||
onStart: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also define a global `onStart` function using `tasks.onStart()`.
|
||||
|
||||
```ts init.ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
|
||||
tasks.onStart(({ ctx, payload, task }) => {
|
||||
console.log(`Run ${ctx.run.id} started on task ${task}`, ctx.run);
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `onStart` function will cause the attempt to fail.</Info>
|
||||
|
||||
### `init` function (deprecated)
|
||||
|
||||
<Warning>
|
||||
The `init` hook is deprecated and will be removed in the future. Use
|
||||
[middleware](/tasks/overview#middleware-and-locals-functions) instead.
|
||||
</Warning>
|
||||
|
||||
This function is called before a run attempt:
|
||||
|
||||
```ts /trigger/init.ts
|
||||
export const taskWithInit = task({
|
||||
id: "task-with-init",
|
||||
init: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can also return data from the `init` function that will be available in the params of the `run`, `cleanup`, `onSuccess`, and `onFailure` functions.
|
||||
|
||||
```ts /trigger/init-return.ts
|
||||
export const taskWithInitReturn = task({
|
||||
id: "task-with-init-return",
|
||||
init: async ({ payload, ctx }) => {
|
||||
return { someData: "someValue" };
|
||||
},
|
||||
run: async (payload: any, { ctx, init }) => {
|
||||
console.log(init.someData); // "someValue"
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `init` function will cause the attempt to fail.</Info>
|
||||
|
||||
### `cleanup` function (deprecated)
|
||||
|
||||
<Warning>
|
||||
The `cleanup` hook is deprecated and will be removed in the future. Use
|
||||
[middleware](/tasks/overview#middleware-and-locals-functions) instead.
|
||||
</Warning>
|
||||
|
||||
This function is called after the `run` function is executed, regardless of whether the run was successful or not. It's useful for cleaning up resources, logging, or other side effects.
|
||||
|
||||
```ts /trigger/cleanup.ts
|
||||
export const taskWithCleanup = task({
|
||||
id: "task-with-cleanup",
|
||||
cleanup: async ({ payload, ctx }) => {
|
||||
//...
|
||||
},
|
||||
run: async (payload: any, { ctx }) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Info>Errors thrown in the `cleanup` function will cause the attempt to fail.</Info>
|
||||
|
||||
## Next steps
|
||||
|
||||
<CardGroup>
|
||||
<Card title="Triggering" icon="bolt" href="/triggering">
|
||||
Learn how to trigger your tasks from your code.
|
||||
</Card>
|
||||
<Card title="Writing tasks" icon="wand-magic-sparkles" href="/writing-tasks-introduction">
|
||||
Tasks are the core of Trigger.dev. Learn how to write them.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -0,0 +1,382 @@
|
||||
---
|
||||
title: "Scheduled tasks (cron)"
|
||||
description: "A task that is triggered on a recurring schedule using cron syntax."
|
||||
---
|
||||
|
||||
<Note>
|
||||
Scheduled tasks are only for recurring tasks. If you want to trigger a one-off task at a future
|
||||
time, you should [use the delay option](/triggering#delay).
|
||||
</Note>
|
||||
|
||||
## Defining a scheduled task
|
||||
|
||||
This task will run when any of the attached schedules trigger. They have a predefined payload with some useful properties:
|
||||
|
||||
```ts
|
||||
import { schedules } from "@trigger.dev/sdk";
|
||||
|
||||
export const firstScheduledTask = schedules.task({
|
||||
id: "first-scheduled-task",
|
||||
run: async (payload) => {
|
||||
//when the task was scheduled to run
|
||||
//note this will be slightly different from new Date() because it takes a few ms to run the task
|
||||
console.log(payload.timestamp); //is a Date object
|
||||
|
||||
//when the task was last run
|
||||
//this can be undefined if it's never been run
|
||||
console.log(payload.lastTimestamp); //is a Date object or undefined
|
||||
|
||||
//the timezone the schedule was registered with, defaults to "UTC"
|
||||
//this is in IANA format, e.g. "America/New_York"
|
||||
//See the full list here: https://cloud.trigger.dev/timezones
|
||||
console.log(payload.timezone); //is a string
|
||||
|
||||
//If you want to output the time in the user's timezone do this:
|
||||
const formatted = payload.timestamp.toLocaleString("en-US", {
|
||||
timeZone: payload.timezone,
|
||||
});
|
||||
|
||||
//the schedule id (you can have many schedules for the same task)
|
||||
//using this you can remove the schedule, update it, etc
|
||||
console.log(payload.scheduleId); //is a string
|
||||
|
||||
//you can optionally provide an external id when creating the schedule
|
||||
//usually you would set this to a userId or some other unique identifier
|
||||
//this can be undefined if you didn't provide one
|
||||
console.log(payload.externalId); //is a string or undefined
|
||||
|
||||
//the next 5 dates this task is scheduled to run
|
||||
console.log(payload.upcoming); //is an array of Date objects
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
You can see from the comments that the payload has several useful properties:
|
||||
|
||||
- `timestamp` - the time the task was scheduled to run, as a UTC date.
|
||||
- `lastTimestamp` - the time the task was last run, as a UTC date.
|
||||
- `timezone` - the timezone the schedule was registered with, defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||
- `scheduleId` - the id of the schedule that triggered the task
|
||||
- `externalId` - the external id you (optionally) provided when creating the schedule
|
||||
- `upcoming` - the next 5 times the task is scheduled to run
|
||||
|
||||
<Note>
|
||||
This task will NOT get triggered on a schedule until you attach a schedule to it. Read on for how
|
||||
to do that.
|
||||
</Note>
|
||||
|
||||
Like all tasks they don't have timeouts, they should be placed inside a [/trigger folder](/config/config-file), and you [can configure them](/tasks/overview#defining-a-task).
|
||||
|
||||
You can set a [TTL](/runs#time-to-live-ttl) on a scheduled task to automatically expire runs that aren't dequeued in time. This is useful when a schedule fires while the previous run is still executing - rather than queueing up stale runs, they'll expire:
|
||||
|
||||
```ts
|
||||
export const frequentTask = schedules.task({
|
||||
id: "frequent-task",
|
||||
ttl: "5m",
|
||||
run: async (payload) => {
|
||||
//...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## How to attach a schedule
|
||||
|
||||
Now that we've defined a scheduled task, we need to define when it will actually run. To do this we need to attach one or more schedules.
|
||||
|
||||
There are two ways of doing this:
|
||||
|
||||
- **Declarative:** defined on your `schedules.task`. They sync when you run the dev command or deploy.
|
||||
- **Imperative:** created from the dashboard or by using the imperative SDK functions like `schedules.create()`.
|
||||
|
||||
<Info>
|
||||
A scheduled task can have multiple schedules attached to it, including a declarative schedule
|
||||
and/or many imperative schedules.
|
||||
</Info>
|
||||
|
||||
### Declarative schedules
|
||||
|
||||
These sync when you run the [dev](/cli-dev-commands) or [deploy](/cli-deploy-commands) commands.
|
||||
|
||||
To create them you add the `cron` property to your `schedules.task()`. This property is optional and is only used if you want to add a declarative schedule to your task:
|
||||
|
||||
```ts
|
||||
export const firstScheduledTask = schedules.task({
|
||||
id: "first-scheduled-task",
|
||||
//every two hours (UTC timezone)
|
||||
cron: "0 */2 * * *",
|
||||
run: async (payload, { ctx }) => {
|
||||
//do something
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you use a string it will be in UTC. Alternatively, you can specify a timezone like this:
|
||||
|
||||
```ts
|
||||
export const secondScheduledTask = schedules.task({
|
||||
id: "second-scheduled-task",
|
||||
cron: {
|
||||
//5am every day Tokyo time
|
||||
pattern: "0 5 * * *",
|
||||
timezone: "Asia/Tokyo",
|
||||
//optional, defaults to all environments
|
||||
//possible values are "PRODUCTION", "STAGING", "PREVIEW" and "DEVELOPMENT"
|
||||
environments: ["PRODUCTION", "STAGING"],
|
||||
},
|
||||
run: async (payload) => {},
|
||||
});
|
||||
```
|
||||
|
||||
When you run the [dev](/cli-dev-commands) or [deploy](/cli-deploy-commands) commands, declarative schedules will be synced. If you add, delete or edit the `cron` property it will be updated when you run these commands. You can view your schedules on the Schedules page in the dashboard.
|
||||
|
||||
### Imperative schedules
|
||||
|
||||
Alternatively you can explicitly attach schedules to a `schedules.task`. You can do this in the Schedules page in the dashboard by just pressing the "New schedule" button, or you can use the SDK to create schedules.
|
||||
|
||||
The advantage of imperative schedules is that they can be created dynamically, for example, you could create a schedule for each user in your database. They can also be activated, disabled, edited, and deleted without deploying new code by using the SDK or dashboard.
|
||||
|
||||
To use imperative schedules you need to do two things:
|
||||
|
||||
1. Define a task in your code using `schedules.task()`.
|
||||
2. Attach 1+ schedules to the task either using the dashboard or the SDK.
|
||||
|
||||
## Supported cron syntax
|
||||
|
||||
```
|
||||
* * * * *
|
||||
┬ ┬ ┬ ┬ ┬
|
||||
│ │ │ │ |
|
||||
│ │ │ │ └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
|
||||
│ │ │ └───── month (1 - 12)
|
||||
│ │ └────────── day of month (1 - 31, L)
|
||||
│ └─────────────── hour (0 - 23)
|
||||
└──────────────────── minute (0 - 59)
|
||||
```
|
||||
|
||||
"L" means the last. In the "day of week" field, 1L means the last Monday of the month. In the "day of month" field, L means the last day of the month.
|
||||
|
||||
We do not support seconds in the cron syntax.
|
||||
|
||||
## When schedules won't trigger
|
||||
|
||||
There are two situations when a scheduled task won't trigger:
|
||||
|
||||
- For Dev environments scheduled tasks will only trigger if you're running the dev CLI.
|
||||
- For Staging/Production environments scheduled tasks will only trigger if the task is in the current deployment (latest version). We won't trigger tasks from previous deployments.
|
||||
|
||||
## Attaching schedules in the dashboard
|
||||
|
||||
You need to attach a schedule to a task before it will run on a schedule. You can attach static schedules in the dashboard:
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Go to the Schedules page">
|
||||
In the sidebar select the "Schedules" page, then press the "New schedule" button. Or you can
|
||||
follow the onboarding and press the create in dashboard button. 
|
||||
</Step>
|
||||
|
||||
<Step title="Create your schedule">
|
||||
Fill in the form and press "Create schedule" when you're done. 
|
||||
|
||||
These are the options when creating a schedule:
|
||||
|
||||
| Name | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------- |
|
||||
| Task | The id of the task you want to attach to. |
|
||||
| Cron pattern | The schedule in cron format. |
|
||||
| Timezone | The timezone the schedule will run in. Defaults to "UTC" |
|
||||
| External id | An optional external id, usually you'd use a userId. |
|
||||
| Deduplication key | An optional deduplication key. If you pass the same value, it will update rather than create. Scoped per project, not per environment. |
|
||||
| Environments | The environments this schedule will run in. |
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Attaching schedules with the SDK
|
||||
|
||||
You call `schedules.create()` to create a schedule from your code. Here's the simplest possible example:
|
||||
|
||||
```ts
|
||||
const createdSchedule = await schedules.create({
|
||||
//The id of the scheduled task you want to attach to.
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in cron format.
|
||||
cron: "0 0 * * *",
|
||||
//this is required, it prevents you from creating duplicate schedules. It will update the schedule if it already exists.
|
||||
deduplicationKey: "my-deduplication-key",
|
||||
});
|
||||
```
|
||||
|
||||
<Note>The `task` id must be a task that you defined using `schedules.task()`.</Note>
|
||||
|
||||
You can create many schedules with the same `task`, `cron`, and `externalId` but only one with the same `deduplicationKey`.
|
||||
|
||||
<Note>
|
||||
The deduplication key is **per project**, not per environment. Using the same key in Production and Staging creates a single schedule; the last create/update decides which environment it appears in. For fixed schedules, prefer **declarative** (cron on the task). If using imperative across environments, use a different deduplication key per environment (e.g. include the env name in the key).
|
||||
</Note>
|
||||
|
||||
This means you can have thousands of schedules attached to a single task, but only one schedule per `deduplicationKey`. Here's an example with all the options:
|
||||
|
||||
```ts
|
||||
const createdSchedule = await schedules.create({
|
||||
//The id of the scheduled task you want to attach to.
|
||||
task: firstScheduledTask.id,
|
||||
//The schedule in cron format.
|
||||
cron: "0 0 * * *",
|
||||
// Optional, it defaults to "UTC". In IANA format, e.g. "America/New_York".
|
||||
// In this case, the task will run at midnight every day in New York time.
|
||||
// If you specify a timezone it will automatically work with daylight saving time.
|
||||
timezone: "America/New_York",
|
||||
//Optionally, you can specify your own IDs (like a user ID) and then use it inside the run function of your task.
|
||||
//This allows you to have per-user cron tasks.
|
||||
externalId: "user_123456",
|
||||
//You can only create one schedule with this key.
|
||||
//If you use it twice, the second call will update the schedule.
|
||||
//This is useful because you don't want to create duplicate schedules for a user.
|
||||
deduplicationKey: "user_123456-todo_reminder",
|
||||
});
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/create) for full details.
|
||||
|
||||
### Dynamic schedules (or multi-tenant schedules)
|
||||
|
||||
By using the `externalId` you can have schedules for your users. This is useful for things like reminders, where you want to have a schedule for each user.
|
||||
|
||||
A reminder task:
|
||||
|
||||
```ts /trigger/reminder.ts
|
||||
import { schedules } from "@trigger.dev/sdk";
|
||||
|
||||
//this task will run when any of the attached schedules trigger
|
||||
export const reminderTask = schedules.task({
|
||||
id: "todo-reminder",
|
||||
run: async (payload) => {
|
||||
if (!payload.externalId) {
|
||||
throw new Error("externalId is required");
|
||||
}
|
||||
|
||||
//get user using the externalId you used when creating the schedule
|
||||
const user = await db.getUser(payload.externalId);
|
||||
|
||||
//send a reminder email
|
||||
await sendReminderEmail(user);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then in your backend code, you can create a schedule for each user:
|
||||
|
||||
```ts Next.js API route
|
||||
import { reminderTask } from "~/trigger/reminder";
|
||||
|
||||
//app/reminders/route.ts
|
||||
export async function POST(request: Request) {
|
||||
//get the JSON from the request
|
||||
const data = await request.json();
|
||||
|
||||
//create a schedule for the user
|
||||
const createdSchedule = await schedules.create({
|
||||
task: reminderTask.id,
|
||||
//8am every day
|
||||
cron: "0 8 * * *",
|
||||
//the user's timezone
|
||||
timezone: data.timezone,
|
||||
//the user id
|
||||
externalId: data.userId,
|
||||
//this makes it impossible to have two reminder schedules for the same user
|
||||
deduplicationKey: `${data.userId}-reminder`,
|
||||
});
|
||||
|
||||
//return a success response with the schedule
|
||||
return Response.json(createdSchedule);
|
||||
}
|
||||
```
|
||||
|
||||
You can also retrieve, list, delete, deactivate and re-activate schedules using the SDK. More on that later.
|
||||
|
||||
## Testing schedules
|
||||
|
||||
You can test a scheduled task in the dashboard. Note that the `scheduleId` will always come through as `sched_1234` to the run.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step title="Go to the Test page">
|
||||
In the sidebar select the "Test" page, then select a scheduled task from the list (they have a
|
||||
clock icon on them) 
|
||||
</Step>
|
||||
|
||||
<Step title="Create your schedule">
|
||||
Fill in the form [1]. You can select from a recent run [2] to pre-populate the fields. Press "Run
|
||||
test" when you're ready 
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Managing schedules with the SDK
|
||||
|
||||
### Retrieving an existing schedule
|
||||
|
||||
```ts
|
||||
const retrievedSchedule = await schedules.retrieve(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/retrieve) for full details.
|
||||
|
||||
### Listing schedules
|
||||
|
||||
```ts
|
||||
const allSchedules = await schedules.list();
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/list) for full details.
|
||||
|
||||
### Updating a schedule
|
||||
|
||||
```ts
|
||||
const updatedSchedule = await schedules.update(scheduleId, {
|
||||
task: firstScheduledTask.id,
|
||||
cron: "0 0 1 * *",
|
||||
externalId: "ext_1234444",
|
||||
deduplicationKey: "my-deduplication-key",
|
||||
});
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/update) for full details.
|
||||
|
||||
### Deactivating a schedule
|
||||
|
||||
```ts
|
||||
const deactivatedSchedule = await schedules.deactivate(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/deactivate) for full details.
|
||||
|
||||
### Activating a schedule
|
||||
|
||||
```ts
|
||||
const activatedSchedule = await schedules.activate(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/activate) for full details.
|
||||
|
||||
### Deleting a schedule
|
||||
|
||||
```ts
|
||||
const deletedSchedule = await schedules.del(scheduleId);
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/delete) for full details.
|
||||
|
||||
### Getting possible timezones
|
||||
|
||||
You might want to show a dropdown menu in your UI so your users can select their timezone. You can get a list of all possible timezones using the SDK:
|
||||
|
||||
```ts
|
||||
const timezones = await schedules.timezones();
|
||||
```
|
||||
|
||||
See [the SDK reference](/management/schedules/timezones) for full details.
|
||||
@@ -0,0 +1,413 @@
|
||||
---
|
||||
title: "schemaTask"
|
||||
sidebarTitle: "Schema task"
|
||||
description: "Define tasks with a runtime payload schema and validate the payload before running the task."
|
||||
---
|
||||
|
||||
The `schemaTask` function allows you to define a task with a runtime payload schema. This schema is used to validate the payload before running the task or when triggering a task directly. If the payload does not match the schema, the task will not execute.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.name, payload.age);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`schemaTask` takes all the same options as [task](/tasks/overview), with the addition of a `schema` field. The `schema` field is a schema parser function from a schema library or or a custom parser function.
|
||||
|
||||
<Note>
|
||||
We will probably eventually combine `task` and `schemaTask` into a single function, but because
|
||||
that would be a breaking change, we are keeping them separate for now.
|
||||
</Note>
|
||||
|
||||
When you trigger the task directly, the payload will be validated against the schema before the [run](/runs) is created:
|
||||
|
||||
```ts
|
||||
import { tasks } from "@trigger.dev/sdk";
|
||||
import { myTask } from "./trigger/myTasks";
|
||||
|
||||
// This will call the schema parser function and validate the payload
|
||||
await myTask.trigger({ name: "Alice", age: "oops" }); // this will throw an error
|
||||
|
||||
// This will NOT call the schema parser function
|
||||
await tasks.trigger<typeof myTask>("my-task", { name: "Alice", age: "oops" }); // this will not throw an error
|
||||
```
|
||||
|
||||
The error thrown when the payload does not match the schema will be the same as the error thrown by the schema parser function. For example, if you are using Zod, the error will be a `ZodError`.
|
||||
|
||||
We will also validate the payload every time before the task is run, so you can be sure that the payload is always valid. In the example above, the task would fail with a `TaskPayloadParsedError` error and skip retrying if the payload does not match the schema.
|
||||
|
||||
## Input/output schemas
|
||||
|
||||
Certain schema libraries, like Zod, split their type inference into "schema in" and "schema out". This means that you can define a single schema that will produce different types when triggering the task and when running the task. For example, you can define a schema that has a default value for a field, or a string coerced into a date:
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
const myTask = schemaTask({
|
||||
id: "my-task",
|
||||
schema: z.object({
|
||||
name: z.string().default("John"),
|
||||
age: z.number(),
|
||||
dob: z.coerce.date(),
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.name, payload.age);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
In this case, the trigger payload type is `{ name?: string, age: number; dob: string }`, but the run payload type is `{ name: string, age: number; dob: Date }`. So you can trigger the task with a payload like this:
|
||||
|
||||
```ts
|
||||
await myTask.trigger({ age: 30, dob: "2020-01-01" }); // this is valid
|
||||
await myTask.trigger({ name: "Alice", age: 30, dob: "2020-01-01" }); // this is also valid
|
||||
```
|
||||
|
||||
## Task-backed AI tools
|
||||
|
||||
Use a `schemaTask` as the implementation of a Vercel [AI SDK](https://vercel.com/docs/ai-sdk) tool: the model calls the tool, and Trigger runs your task as a **subtask** with tool-call metadata, optional [chat context](/ai-chat/patterns/sub-agents), and the same payload validation as a normal trigger.
|
||||
|
||||
### Recommended: `ai.toolExecute` with `tool()`
|
||||
|
||||
Prefer building the tool with the AI SDK’s [`tool()`](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling) and passing **`execute: ai.toolExecute(yourTask)`**. You keep full control of `description`, `inputSchema`, and AI-SDK-only options (for example `experimental_toToolResultContent`), and your types follow the `ai` version installed in **your** app.
|
||||
|
||||
```ts
|
||||
import { ai } from "@trigger.dev/sdk/ai";
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { tool, generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { z } from "zod";
|
||||
|
||||
const myToolTask = schemaTask({
|
||||
id: "my-tool-task",
|
||||
schema: z.object({
|
||||
foo: z.string(),
|
||||
}),
|
||||
run: async ({ foo }) => {
|
||||
return { bar: foo.toUpperCase() };
|
||||
},
|
||||
});
|
||||
|
||||
const myTool = tool({
|
||||
description: myToolTask.description ?? "",
|
||||
inputSchema: myToolTask.schema!,
|
||||
execute: ai.toolExecute(myToolTask),
|
||||
});
|
||||
|
||||
export const myAiTask = schemaTask({
|
||||
id: "my-ai-task",
|
||||
schema: z.object({
|
||||
text: z.string(),
|
||||
}),
|
||||
run: async ({ text }) => {
|
||||
const { text: reply } = await generateText({
|
||||
prompt: text,
|
||||
model: openai("gpt-4o"),
|
||||
tools: {
|
||||
myTool,
|
||||
},
|
||||
});
|
||||
return reply;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`experimental_toToolResultContent` and other tool-level options belong on **`tool({ ... })`**, not on `ai.toolExecute`:
|
||||
|
||||
```ts
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { Sandbox } from "@e2b/code-interpreter";
|
||||
import { ai } from "@trigger.dev/sdk/ai";
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { generateObject, tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
const chartTask = schemaTask({
|
||||
id: "chart",
|
||||
description: "Generate a chart using natural language",
|
||||
schema: z.object({
|
||||
input: z.string().describe("The chart to generate"),
|
||||
}),
|
||||
run: async ({ input }) => {
|
||||
const code = await generateObject({
|
||||
model: openai("gpt-4o"),
|
||||
schema: z.object({
|
||||
code: z.string().describe("The Python code to execute"),
|
||||
}),
|
||||
system: `You are a helpful assistant that generates matplotlib code. End with plt.show().`,
|
||||
prompt: input,
|
||||
});
|
||||
|
||||
const sandbox = await Sandbox.create();
|
||||
const execution = await sandbox.runCode(code.object.code);
|
||||
const firstResult = execution.results[0];
|
||||
|
||||
if (firstResult.png) {
|
||||
return { chart: firstResult.png };
|
||||
}
|
||||
throw new Error("No chart generated");
|
||||
},
|
||||
});
|
||||
|
||||
export const chartTool = tool({
|
||||
description: chartTask.description ?? "",
|
||||
inputSchema: chartTask.schema!,
|
||||
execute: ai.toolExecute(chartTask),
|
||||
experimental_toToolResultContent: (result) => [
|
||||
{ type: "image", data: result.chart, mimeType: "image/png" },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
Inside the task run, you can read tool execution context with **`ai.currentToolOptions()`** (and helpers like `ai.toolCallId()`, `ai.chatContext()` when running inside a [`chat.agent`](/ai-chat/overview)):
|
||||
|
||||
```ts
|
||||
import { ai } from "@trigger.dev/sdk/ai";
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
const myToolTask = schemaTask({
|
||||
id: "my-tool-task",
|
||||
schema: z.object({
|
||||
foo: z.string(),
|
||||
}),
|
||||
run: async ({ foo }) => {
|
||||
const toolOptions = ai.currentToolOptions();
|
||||
console.log(toolOptions);
|
||||
return { foo };
|
||||
},
|
||||
});
|
||||
|
||||
export const myTool = tool({
|
||||
description: myToolTask.description ?? "",
|
||||
inputSchema: myToolTask.schema!,
|
||||
execute: ai.toolExecute(myToolTask),
|
||||
});
|
||||
```
|
||||
|
||||
See the [AI SDK tool execution options](https://sdk.vercel.ai/docs/ai-sdk-core/tools-and-tool-calling#tool-execution-options) for fields passed through the runtime.
|
||||
|
||||
<Note>
|
||||
`ai.toolExecute` works with `schemaTask` definitions that use Zod, ArkType, or any schema that provides a JSON schema via `.toJsonSchema()` (same coverage as the legacy `ai.tool` wrapper).
|
||||
</Note>
|
||||
|
||||
### Deprecated: `ai.tool`
|
||||
|
||||
The **`ai.tool(task, options?)`** helper is **deprecated**. It constructs an AI SDK `Tool` for you (using `tool()` for Zod-like schemas and `dynamicTool()` otherwise) and may be removed in a future major version. New code should use **`tool({ ..., execute: ai.toolExecute(task) })`** as shown above.
|
||||
|
||||
### Legacy `ai.tool` example (deprecated)
|
||||
|
||||
```ts
|
||||
import { ai } from "@trigger.dev/sdk/ai";
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
|
||||
const myToolTask = schemaTask({
|
||||
id: "my-tool-task",
|
||||
schema: z.object({ foo: z.string() }),
|
||||
run: async ({ foo }) => ({ foo }),
|
||||
});
|
||||
|
||||
// Deprecated — prefer tool({ execute: ai.toolExecute(myToolTask), ... })
|
||||
const myTool = ai.tool(myToolTask);
|
||||
```
|
||||
|
||||
## Supported schema types
|
||||
|
||||
### Zod
|
||||
|
||||
You can use the [Zod](https://zod.dev) schema library to define your schema. The schema will be validated using Zod's `parse` function.
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { z } from "zod";
|
||||
|
||||
export const zodTask = schemaTask({
|
||||
id: "types/zod",
|
||||
schema: z.object({
|
||||
bar: z.string(),
|
||||
baz: z.string().default("foo"),
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Yup
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import * as yup from "yup";
|
||||
|
||||
export const yupTask = schemaTask({
|
||||
id: "types/yup",
|
||||
schema: yup.object({
|
||||
bar: yup.string().required(),
|
||||
baz: yup.string().default("foo"),
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Superstruct
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { object, string } from "superstruct";
|
||||
|
||||
export const superstructTask = schemaTask({
|
||||
id: "types/superstruct",
|
||||
schema: object({
|
||||
bar: string(),
|
||||
baz: string(),
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### ArkType
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { type } from "arktype";
|
||||
|
||||
export const arktypeTask = schemaTask({
|
||||
id: "types/arktype",
|
||||
schema: type({
|
||||
bar: "string",
|
||||
baz: "string",
|
||||
}).assert,
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### @effect/schema
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import * as Schema from "@effect/schema/Schema";
|
||||
|
||||
// For some funny typescript reason, you cannot pass the Schema.decodeUnknownSync directly to schemaTask
|
||||
const effectSchemaParser = Schema.decodeUnknownSync(
|
||||
Schema.Struct({ bar: Schema.String, baz: Schema.String })
|
||||
);
|
||||
|
||||
export const effectTask = schemaTask({
|
||||
id: "types/effect",
|
||||
schema: effectSchemaParser,
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### runtypes
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import * as T from "runtypes";
|
||||
|
||||
export const runtypesTask = schemaTask({
|
||||
id: "types/runtypes",
|
||||
schema: T.Record({
|
||||
bar: T.String,
|
||||
baz: T.String,
|
||||
}),
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### valibot
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
|
||||
import * as v from "valibot";
|
||||
|
||||
// For some funny typescript reason, you cannot pass the v.parser directly to schemaTask
|
||||
const valibotParser = v.parser(
|
||||
v.object({
|
||||
bar: v.string(),
|
||||
baz: v.string(),
|
||||
})
|
||||
);
|
||||
|
||||
export const valibotTask = schemaTask({
|
||||
id: "types/valibot",
|
||||
schema: valibotParser,
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### typebox
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { wrap } from "@typeschema/typebox";
|
||||
|
||||
export const typeboxTask = schemaTask({
|
||||
id: "types/typebox",
|
||||
schema: wrap(
|
||||
Type.Object({
|
||||
bar: Type.String(),
|
||||
baz: Type.String(),
|
||||
})
|
||||
),
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Custom parser function
|
||||
|
||||
You can also define a custom parser function that will be called with the payload before the task is run. The parser function should return the parsed payload or throw an error if the payload is invalid.
|
||||
|
||||
```ts
|
||||
import { schemaTask } from "@trigger.dev/sdk";
|
||||
|
||||
export const customParserTask = schemaTask({
|
||||
id: "types/custom-parser",
|
||||
schema: (data: unknown) => {
|
||||
// This is a custom parser, and should do actual parsing (not just casting)
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data");
|
||||
}
|
||||
|
||||
const { bar, baz } = data as { bar: string; baz: string };
|
||||
|
||||
return { bar, baz };
|
||||
},
|
||||
run: async (payload) => {
|
||||
console.log(payload.bar, payload.baz);
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,884 @@
|
||||
---
|
||||
title: "Streaming data from tasks"
|
||||
sidebarTitle: "Streams"
|
||||
description: "Pipe continuous data from your Trigger.dev tasks to frontend or backend clients in real time. Stream AI completions, file chunks, progress updates, and more."
|
||||
---
|
||||
|
||||
**Streams let you pipe data from a running task to your frontend or backend as it's produced.** Think AI completions token by token, progress updates, or file chunks. You can also **send data into** running tasks with [Input Streams](#input-streams) for bidirectional flows (cancel buttons, approvals).
|
||||
|
||||
For subscribing to **run state changes** (status, metadata, tags) instead, see [Realtime](/realtime/overview).
|
||||
|
||||
<Note>
|
||||
Streams require SDK version **4.1.0 or later** (`@trigger.dev/sdk` and `@trigger.dev/react-hooks`).
|
||||
This doc describes the current streams behavior (v2 is the default). For pre-4.1.0 streams, see
|
||||
[Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) below.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
Streams provide:
|
||||
|
||||
- **Unlimited stream length** (previously capped at 2000 chunks)
|
||||
- **Unlimited active streams per run** (previously 5)
|
||||
- **Improved reliability** with automatic resumption on connection loss
|
||||
- **28-day stream retention** (previously 1 day)
|
||||
- **Multiple client streams** can pipe to a single stream
|
||||
- **Enhanced dashboard visibility** for viewing stream data in real-time
|
||||
|
||||
Streams v2 is the **default** when using SDK 4.1.0 or later. If you trigger tasks outside the SDK, set the `x-trigger-realtime-streams-version=v2` header. To opt out, use `auth.configure({ future: { v2RealtimeStreams: false } })` or `TRIGGER_V2_REALTIME_STREAMS=0`.
|
||||
|
||||
## Limits Comparison
|
||||
|
||||
| Limit | Legacy (pre-4.1.0) | Current |
|
||||
| -------------------------------- | ------------------ | --------- |
|
||||
| Maximum stream length | 2000 | Unlimited |
|
||||
| Number of active streams per run | 5 | Unlimited |
|
||||
| Maximum streams per run | 10 | Unlimited |
|
||||
| Maximum stream TTL | 1 day | 28 days |
|
||||
| Maximum stream size | 10MB | 300 MiB |
|
||||
|
||||
## Quick Start
|
||||
|
||||
The recommended workflow for **output** streams (data from task to client):
|
||||
|
||||
1. **Define your streams** in a shared location using `streams.define()`
|
||||
2. **Use the defined stream** in your tasks with `.pipe()`, `.append()`, or `.writer()`
|
||||
3. **Read from the stream** using `.read()` or the `useRealtimeStream` hook in React
|
||||
|
||||
This approach gives you full type safety, better code organization, and easier maintenance as your application grows. For **input** streams (sending data into a running task), see [Input Streams](#input-streams) below.
|
||||
|
||||
## Defining Typed Streams (Recommended)
|
||||
|
||||
The recommended way to work with streams is to define them once with `streams.define()`. This allows you to specify the chunk type and stream ID in one place, and then reuse that definition throughout your codebase with full type safety.
|
||||
|
||||
### Creating a Defined Stream
|
||||
|
||||
Define your streams in a shared location (like `app/streams.ts` or `trigger/streams.ts`):
|
||||
|
||||
```ts
|
||||
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
||||
|
||||
// Define a stream with a specific type
|
||||
export const aiStream = streams.define<string>({
|
||||
id: "ai-output",
|
||||
});
|
||||
|
||||
// Export the type for use in frontend components
|
||||
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
||||
```
|
||||
|
||||
You can define streams for any JSON-serializable type:
|
||||
|
||||
```ts
|
||||
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
||||
import { UIMessageChunk } from "ai";
|
||||
|
||||
// Stream for AI UI message chunks
|
||||
export const aiStream = streams.define<UIMessageChunk>({
|
||||
id: "ai",
|
||||
});
|
||||
|
||||
// Stream for progress updates
|
||||
export const progressStream = streams.define<{ step: string; percent: number }>({
|
||||
id: "progress",
|
||||
});
|
||||
|
||||
// Stream for simple text
|
||||
export const logStream = streams.define<string>({
|
||||
id: "logs",
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
||||
export type ProgressStreamPart = InferStreamType<typeof progressStream>;
|
||||
export type LogStreamPart = InferStreamType<typeof logStream>;
|
||||
```
|
||||
|
||||
### Using Defined Streams in Tasks
|
||||
|
||||
Once defined, you can use all stream methods on your defined stream:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { aiStream } from "./streams";
|
||||
|
||||
export const streamTask = task({
|
||||
id: "stream-task",
|
||||
run: async (payload: { prompt: string }) => {
|
||||
// Get a stream from an AI service, database, etc.
|
||||
const stream = await getAIStream(payload.prompt);
|
||||
|
||||
// Pipe the stream using your defined stream
|
||||
const { stream: readableStream, waitUntilComplete } = aiStream.pipe(stream);
|
||||
|
||||
// Option A: Iterate over the stream locally
|
||||
for await (const chunk of readableStream) {
|
||||
console.log("Received chunk:", chunk);
|
||||
}
|
||||
|
||||
// Option B: Wait for the stream to complete
|
||||
await waitUntilComplete();
|
||||
|
||||
return { message: "Stream completed" };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Reading from a Stream
|
||||
|
||||
Use the defined stream's `read()` method to consume data from anywhere (frontend, backend, or another task):
|
||||
|
||||
```ts
|
||||
import { aiStream } from "./streams";
|
||||
|
||||
const stream = await aiStream.read(runId);
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log(chunk); // chunk is typed as the stream's chunk type
|
||||
}
|
||||
```
|
||||
|
||||
With options:
|
||||
|
||||
```ts
|
||||
const stream = await aiStream.read(runId, {
|
||||
timeoutInSeconds: 60, // Stop if no data for 60 seconds
|
||||
startIndex: 10, // Start from the 10th chunk
|
||||
});
|
||||
```
|
||||
|
||||
#### Appending to a Stream
|
||||
|
||||
Use the defined stream's `append()` method to add a single chunk:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { aiStream, progressStream, logStream } from "./streams";
|
||||
|
||||
export const appendTask = task({
|
||||
id: "append-task",
|
||||
run: async (payload) => {
|
||||
// Append to different streams with full type safety
|
||||
await logStream.append("Processing started");
|
||||
await progressStream.append({ step: "Initialization", percent: 0 });
|
||||
|
||||
// Do some work...
|
||||
|
||||
await progressStream.append({ step: "Processing", percent: 50 });
|
||||
await logStream.append("Step 1 complete");
|
||||
|
||||
// Do more work...
|
||||
|
||||
await progressStream.append({ step: "Complete", percent: 100 });
|
||||
await logStream.append("All steps complete");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Writing Multiple Chunks
|
||||
|
||||
Use the defined stream's `writer()` method for more complex stream writing:
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { logStream } from "./streams";
|
||||
|
||||
export const writerTask = task({
|
||||
id: "writer-task",
|
||||
run: async (payload) => {
|
||||
const { waitUntilComplete } = logStream.writer({
|
||||
execute: ({ write, merge }) => {
|
||||
// Write individual chunks
|
||||
write("Chunk 1");
|
||||
write("Chunk 2");
|
||||
|
||||
// Merge another stream
|
||||
const additionalStream = ReadableStream.from(["Chunk 3", "Chunk 4", "Chunk 5"]);
|
||||
merge(additionalStream);
|
||||
},
|
||||
});
|
||||
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Using Defined Streams in React
|
||||
|
||||
Defined streams work seamlessly with the `useRealtimeStream` hook:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
import { aiStream } from "@/app/streams";
|
||||
|
||||
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
||||
// Pass the defined stream directly - full type safety!
|
||||
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
||||
accessToken,
|
||||
timeoutInSeconds: 600,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Direct Stream Methods (Without Defining)
|
||||
|
||||
<Warning>
|
||||
We strongly recommend using `streams.define()` instead of direct methods. Defined streams provide
|
||||
better organization, full type safety, and make it easier to maintain your codebase as it grows.
|
||||
</Warning>
|
||||
|
||||
If you have a specific reason to avoid defined streams, you can use stream methods directly by specifying the stream key each time.
|
||||
|
||||
### Direct Piping
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const directStreamTask = task({
|
||||
id: "direct-stream",
|
||||
run: async (payload: { prompt: string }) => {
|
||||
const stream = await getAIStream(payload.prompt);
|
||||
|
||||
// Specify the stream key directly
|
||||
const { stream: readableStream, waitUntilComplete } = streams.pipe("ai-output", stream);
|
||||
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Direct Reading
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
|
||||
// Specify the stream key when reading
|
||||
const stream = await streams.read(runId, "ai-output");
|
||||
|
||||
for await (const chunk of stream) {
|
||||
console.log(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Appending
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const directAppendTask = task({
|
||||
id: "direct-append",
|
||||
run: async (payload) => {
|
||||
// Specify the stream key each time
|
||||
await streams.append("logs", "Processing started");
|
||||
await streams.append("progress", "50%");
|
||||
await streams.append("logs", "Complete");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Direct Writing
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const directWriterTask = task({
|
||||
id: "direct-writer",
|
||||
run: async (payload) => {
|
||||
const { waitUntilComplete } = streams.writer("output", {
|
||||
execute: ({ write, merge }) => {
|
||||
write("Chunk 1");
|
||||
write("Chunk 2");
|
||||
},
|
||||
});
|
||||
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Default Stream
|
||||
|
||||
Every run has a "default" stream, allowing you to skip the stream key entirely. This is useful for simple cases where you only need one stream per run.
|
||||
|
||||
Using direct methods:
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const defaultStreamTask = task({
|
||||
id: "default-stream",
|
||||
run: async (payload) => {
|
||||
const stream = getDataStream();
|
||||
|
||||
// No stream key needed - uses "default"
|
||||
const { waitUntilComplete } = streams.pipe(stream);
|
||||
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
|
||||
// Reading from the default stream
|
||||
const readStream = await streams.read(runId);
|
||||
```
|
||||
|
||||
## Targeting Different Runs
|
||||
|
||||
You can pipe streams to parent, root, or any other run using the `target` option. This works with both defined streams and direct methods.
|
||||
|
||||
### With Defined Streams
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { logStream } from "./streams";
|
||||
|
||||
export const childTask = task({
|
||||
id: "child-task",
|
||||
run: async (payload, { ctx }) => {
|
||||
const stream = getDataStream();
|
||||
|
||||
// Pipe to parent run
|
||||
logStream.pipe(stream, { target: "parent" });
|
||||
|
||||
// Pipe to root run
|
||||
logStream.pipe(stream, { target: "root" });
|
||||
|
||||
// Pipe to self (default behavior)
|
||||
logStream.pipe(stream, { target: "self" });
|
||||
|
||||
// Pipe to a specific run ID
|
||||
logStream.pipe(stream, { target: payload.otherRunId });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### With Direct Methods
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const childTask = task({
|
||||
id: "child-task",
|
||||
run: async (payload, { ctx }) => {
|
||||
const stream = getDataStream();
|
||||
|
||||
// Pipe to parent run
|
||||
streams.pipe("output", stream, { target: "parent" });
|
||||
|
||||
// Pipe to root run
|
||||
streams.pipe("output", stream, { target: "root" });
|
||||
|
||||
// Pipe to a specific run ID
|
||||
streams.pipe("output", stream, { target: payload.otherRunId });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Streaming from Outside a Task
|
||||
|
||||
If you specify a `target` run ID, you can pipe streams from anywhere (like a Next.js API route):
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { streamText } from "ai";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages, runId } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o"),
|
||||
messages,
|
||||
});
|
||||
|
||||
// Pipe AI stream to a Trigger.dev run
|
||||
const { stream } = streams.pipe("ai-stream", result.toUIMessageStream(), {
|
||||
target: runId,
|
||||
});
|
||||
|
||||
return new Response(stream as any, {
|
||||
headers: { "Content-Type": "text/event-stream" },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## React Hook
|
||||
|
||||
Use the `useRealtimeStream` hook to subscribe to streams in your React components.
|
||||
|
||||
### With Defined Streams (Recommended)
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
import { aiStream } from "@/app/streams";
|
||||
|
||||
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
||||
// Pass the defined stream directly for full type safety
|
||||
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
||||
accessToken,
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### With Direct Stream Keys
|
||||
|
||||
If you prefer not to use defined streams, you can specify the stream key directly:
|
||||
|
||||
```tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
|
||||
export function StreamViewer({ accessToken, runId }: { accessToken: string; runId: string }) {
|
||||
const { parts, error } = useRealtimeStream<string>(runId, "ai-output", {
|
||||
accessToken,
|
||||
timeoutInSeconds: 600,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Default Stream
|
||||
|
||||
```tsx
|
||||
// Omit stream key to use the default stream
|
||||
const { parts, error } = useRealtimeStream<string>(runId, {
|
||||
accessToken,
|
||||
});
|
||||
```
|
||||
|
||||
### Hook Options
|
||||
|
||||
```tsx
|
||||
const { parts, error } = useRealtimeStream(streamDef, runId, {
|
||||
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
|
||||
});
|
||||
```
|
||||
|
||||
## Input Streams
|
||||
|
||||
Input Streams let you send data **into** a running task from your backend or frontend. While output streams (above) send data out of tasks, input streams complete the loop — enabling bidirectional communication.
|
||||
|
||||
<Note>
|
||||
Input Streams require SDK version **4.4.2 or later** and use the same streams infrastructure (v2 is the default). If you're on an older SDK, calling `.on()` or `.once()` will throw with instructions to enable v2 streams. See [Pre-4.1.0 streams (legacy)](#pre-410-streams-legacy) for the older metadata-based API.
|
||||
</Note>
|
||||
|
||||
### Input Streams overview
|
||||
|
||||
Input Streams solve three common problems:
|
||||
|
||||
- **Cancelling AI streams mid-generation.** When you use AI SDK's `streamText` inside a task, the LLM keeps generating until it's done — even if the user clicked "Stop." With input streams, your frontend sends a cancel signal and the task aborts the LLM call immediately.
|
||||
- **Human-in-the-loop workflows.** A task generates a draft, then pauses and waits for the user to approve or edit it before continuing.
|
||||
- **Interactive agents.** An AI agent running as a task needs follow-up information from the user mid-execution — clarifying a question, choosing between options, or providing additional context.
|
||||
|
||||
### Quick Start (Input Streams)
|
||||
|
||||
1. **Define** input streams in a shared file with `streams.input<T>({ id: "..." })`.
|
||||
2. **Receive** in your task with `.wait()`, `.once()`, `.on()`, or `.peek()`.
|
||||
3. **Send** from your backend with `.send(runId, data)` or from the frontend with the `useInputStreamSend` hook (see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
|
||||
|
||||
### Defining Input Streams
|
||||
|
||||
Use `streams.input()` to define a typed input stream. The generic parameter controls the shape of data that can be sent:
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
|
||||
export const cancelSignal = streams.input<{ reason?: string }>({
|
||||
id: "cancel",
|
||||
});
|
||||
|
||||
export const approval = streams.input<{ approved: boolean; reviewer: string }>({
|
||||
id: "approval",
|
||||
});
|
||||
|
||||
export const userResponse = streams.input<{
|
||||
action: "approve" | "reject" | "edit";
|
||||
message?: string;
|
||||
edits?: Record<string, string>;
|
||||
}>({
|
||||
id: "user-response",
|
||||
});
|
||||
```
|
||||
|
||||
Type safety is enforced through the generic — both `.send()` and the receiving methods (`.wait()`, `.once()`, `.on()`, `.peek()`) share the same type.
|
||||
|
||||
### Receiving data inside a task
|
||||
|
||||
| Method | Task suspended? | Compute cost while waiting | Best for |
|
||||
|--------|-----------------|----------------------------|-----------|
|
||||
| `.wait()` | **Yes** | **None** — process freed | Approval gates, human-in-the-loop, long waits |
|
||||
| `.once()` | No | Full — process stays alive | Short waits, concurrent work; returns result object with `.unwrap()` |
|
||||
| `.on(handler)` | No | Full — process stays alive | Continuous listening (cancel signals, live updates) |
|
||||
| `.peek()` | No | None | Non-blocking check for latest buffered value |
|
||||
|
||||
#### `wait()` — Suspend until data arrives
|
||||
|
||||
Suspends the task entirely, freeing compute resources. The task resumes when data arrives via `.send()`. Returns a [`ManualWaitpointPromise`](/wait-for-token) — the same type as `wait.forToken()`.
|
||||
|
||||
```ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { approval } from "./streams";
|
||||
|
||||
export const publishPost = task({
|
||||
id: "publish-post",
|
||||
run: async (payload: { postId: string }) => {
|
||||
const draft = await prepareDraft(payload.postId);
|
||||
await notifyReviewer(draft);
|
||||
|
||||
const result = await approval.wait({ timeout: "7d" });
|
||||
|
||||
if (result.ok) {
|
||||
if (result.output.approved) {
|
||||
await publish(draft);
|
||||
return { published: true, reviewer: result.output.reviewer };
|
||||
}
|
||||
return { published: false, reviewer: result.output.reviewer };
|
||||
}
|
||||
return { published: false, timedOut: true };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Use `.unwrap()` to throw on timeout: `const data = await approval.wait({ timeout: "24h" }).unwrap();`
|
||||
|
||||
**Options:** `timeout` (e.g. `"30s"`, `"5m"`, `"24h"`, `"7d"`), `idempotencyKey`, `idempotencyKeyTTL`, `tags`. Use `idempotencyKey` when your task has retries so the same waitpoint is resumed across retries.
|
||||
|
||||
#### `once()` — Wait for the next value (non-suspending)
|
||||
|
||||
Blocks until data arrives but keeps the task process alive. Returns a result object; use `.unwrap()` to get the data or throw on timeout.
|
||||
|
||||
```ts
|
||||
const result = await approval.once({ timeoutMs: 300_000 });
|
||||
if (result.ok) {
|
||||
console.log(result.output.approved);
|
||||
}
|
||||
// Or: const data = await approval.once({ timeoutMs: 300_000 }).unwrap();
|
||||
```
|
||||
|
||||
`once()` also accepts a `signal` (e.g. `AbortController.signal`) for cancellation.
|
||||
|
||||
#### `on()` — Listen for every value
|
||||
|
||||
Registers a persistent handler that fires on every piece of data. Handlers are automatically cleaned up when the task run completes. Call `.off()` on the returned subscription to stop listening early.
|
||||
|
||||
```ts
|
||||
const controller = new AbortController();
|
||||
cancelSignal.on((data) => {
|
||||
console.log("Cancelled:", data.reason);
|
||||
controller.abort();
|
||||
});
|
||||
const result = streamText({ ..., abortSignal: controller.signal });
|
||||
```
|
||||
|
||||
#### `peek()` — Non-blocking check
|
||||
|
||||
Returns the most recent buffered value without waiting, or `undefined` if nothing has been received yet.
|
||||
|
||||
```ts
|
||||
const latest = cancelSignal.peek();
|
||||
if (latest) {
|
||||
// A cancel was already sent before we checked
|
||||
}
|
||||
```
|
||||
|
||||
### Sending data to a running task
|
||||
|
||||
Use `.send(runId, data)` from your backend to push data into a running task. See the [backend input streams guide](/realtime/backend/input-streams) for API route patterns.
|
||||
|
||||
```ts
|
||||
import { cancelSignal, approval } from "./trigger/streams";
|
||||
|
||||
await cancelSignal.send(runId, { reason: "User clicked stop" });
|
||||
await approval.send(runId, { approved: true, reviewer: "alice@example.com" });
|
||||
```
|
||||
|
||||
### Complete example: Cancellable AI streaming
|
||||
|
||||
Stream an AI response while allowing the user to cancel mid-generation.
|
||||
|
||||
**Define the streams:**
|
||||
|
||||
```ts
|
||||
import { streams } from "@trigger.dev/sdk";
|
||||
|
||||
export const aiOutput = streams.define<string>({ id: "ai" });
|
||||
export const cancelStream = streams.input<{ reason?: string }>({ id: "cancel" });
|
||||
```
|
||||
|
||||
**Task:** Register `cancelStream.on()` to abort an `AbortController`, then pipe `streamText(...).textStream` to `aiOutput`. **Backend:** POST to an API route that calls `cancelStream.send(runId, { reason: "User clicked stop" })`. **Frontend:** Use `useRealtimeStream(aiOutput, runId, { accessToken })` and a button that calls your cancel API (or use the `useInputStreamSend` hook; see [Realtime React hooks](/realtime/react-hooks/streams#useinputstreamsend)).
|
||||
|
||||
**Important notes (input streams):** You cannot send to a completed, failed, or canceled run. Max payload per `.send()` is 1MB. Data sent before a listener is registered is buffered and delivered when a listener attaches; `.wait()` handles the buffering race automatically. Use `.wait()` for long waits to free compute; use `.once()` for short waits or concurrent work. Define input streams in a shared location and combine with output streams for full bidirectional communication.
|
||||
|
||||
## Complete Example: AI Streaming
|
||||
|
||||
### Define the stream
|
||||
|
||||
```ts
|
||||
// app/streams.ts
|
||||
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
||||
import { UIMessageChunk } from "ai";
|
||||
|
||||
export const aiStream = streams.define<UIMessageChunk>({
|
||||
id: "ai",
|
||||
});
|
||||
|
||||
export type AIStreamPart = InferStreamType<typeof aiStream>;
|
||||
```
|
||||
|
||||
### Create the task
|
||||
|
||||
```ts
|
||||
// trigger/ai-task.ts
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { streamText } from "ai";
|
||||
import { aiStream } from "@/app/streams";
|
||||
|
||||
export const generateAI = task({
|
||||
id: "generate-ai",
|
||||
run: async (payload: { prompt: string }) => {
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o"),
|
||||
prompt: payload.prompt,
|
||||
});
|
||||
|
||||
const { waitUntilComplete } = aiStream.pipe(result.toUIMessageStream());
|
||||
|
||||
await waitUntilComplete();
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Frontend component
|
||||
|
||||
```tsx
|
||||
// components/ai-stream.tsx
|
||||
"use client";
|
||||
|
||||
import { useRealtimeStream } from "@trigger.dev/react-hooks";
|
||||
import { aiStream } from "@/app/streams";
|
||||
|
||||
export function AIStream({ accessToken, runId }: { accessToken: string; runId: string }) {
|
||||
const { parts, error } = useRealtimeStream(aiStream, runId, {
|
||||
accessToken,
|
||||
timeoutInSeconds: 300,
|
||||
});
|
||||
|
||||
if (error) return <div>Error: {error.message}</div>;
|
||||
if (!parts) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="prose">
|
||||
{parts.map((part, i) => (
|
||||
<span key={i}>{part}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from v1
|
||||
|
||||
If you're using the old `metadata.stream()` API, here's how to migrate to the recommended v2 approach:
|
||||
|
||||
### Step 1: Define Your Streams
|
||||
|
||||
Create a shared streams definition file:
|
||||
|
||||
```ts
|
||||
// app/streams.ts or trigger/streams.ts
|
||||
import { streams, InferStreamType } from "@trigger.dev/sdk";
|
||||
|
||||
export const myStream = streams.define<string>({
|
||||
id: "my-stream",
|
||||
});
|
||||
|
||||
export type MyStreamPart = InferStreamType<typeof myStream>;
|
||||
```
|
||||
|
||||
### Step 2: Update Your Tasks
|
||||
|
||||
Replace `metadata.stream()` with the defined stream's `pipe()` method:
|
||||
|
||||
```ts
|
||||
// Before (v1)
|
||||
import { metadata, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload) => {
|
||||
const stream = getDataStream();
|
||||
await metadata.stream("my-stream", stream);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// After (v2 - Recommended)
|
||||
import { task } from "@trigger.dev/sdk";
|
||||
import { myStream } from "./streams";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload) => {
|
||||
const stream = getDataStream();
|
||||
|
||||
// Don't await - returns immediately
|
||||
const { waitUntilComplete } = myStream.pipe(stream);
|
||||
|
||||
// Optionally wait for completion
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Step 3: Update Your Frontend
|
||||
|
||||
Use the defined stream with `useRealtimeStream`:
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
const { parts, error } = useRealtimeStream<string>(runId, "my-stream", {
|
||||
accessToken,
|
||||
});
|
||||
```
|
||||
|
||||
```tsx
|
||||
// After
|
||||
import { myStream } from "@/app/streams";
|
||||
|
||||
const { parts, error } = useRealtimeStream(myStream, runId, {
|
||||
accessToken,
|
||||
});
|
||||
```
|
||||
|
||||
### Alternative: Direct Methods (Not Recommended)
|
||||
|
||||
If you prefer not to use defined streams, you can use direct methods:
|
||||
|
||||
```ts
|
||||
import { streams, task } from "@trigger.dev/sdk";
|
||||
|
||||
export const myTask = task({
|
||||
id: "my-task",
|
||||
run: async (payload) => {
|
||||
const stream = getDataStream();
|
||||
const { waitUntilComplete } = streams.pipe("my-stream", stream);
|
||||
await waitUntilComplete();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Reliability Features
|
||||
|
||||
Streams v2 includes automatic reliability improvements:
|
||||
|
||||
- **Automatic resumption**: If a connection is lost, both appending and reading will automatically resume from the last successful chunk
|
||||
- **No data loss**: Network issues won't cause stream data to be lost
|
||||
- **Idempotent operations**: Duplicate chunks are automatically handled
|
||||
|
||||
These improvements happen automatically - no code changes needed.
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
Streams are now visible in the Trigger.dev dashboard, allowing you to:
|
||||
|
||||
- View stream data in real-time as it's generated
|
||||
- Inspect historical stream data for completed runs
|
||||
- Debug streaming issues with full visibility into chunk delivery
|
||||
|
||||
<video src="https://content.trigger.dev/streams-v2-dashboard.mp4" controls muted autoPlay loop />
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use `streams.define()`**: Define your streams in a shared location for better organization, type safety, and code reusability. This is the recommended approach for all streams.
|
||||
2. **Export stream types**: Use `InferStreamType` to export types for your frontend components
|
||||
3. **Handle errors gracefully**: Always check for errors when reading streams in your UI
|
||||
4. **Set appropriate timeouts**: Adjust `timeoutInSeconds` based on your use case (AI completions may need longer timeouts)
|
||||
5. **Target parent runs**: When orchestrating with child tasks, pipe to parent runs for easier consumption
|
||||
6. **Throttle frontend updates**: Use `throttleInMs` in `useRealtimeStream` to prevent excessive re-renders
|
||||
7. **Use descriptive stream IDs**: Choose clear, descriptive IDs like `"ai-output"` or `"progress"` instead of generic names
|
||||
|
||||
## Pre-4.1.0 streams (legacy)
|
||||
|
||||
Prior to SDK 4.1.0, streams used the older metadata-based API. If you're on an earlier version, see [metadata.stream()](/runs/metadata#stream) for legacy usage. With 4.4.2+, [Input Streams](#input-streams) are available and documented in this page.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Stream not appearing in dashboard
|
||||
|
||||
- Verify your task is actually writing to the stream
|
||||
- Check that the stream key matches between writing and reading
|
||||
|
||||
### Stream timeout errors
|
||||
|
||||
- Increase `timeoutInSeconds` in your `read()` or `useRealtimeStream()` calls
|
||||
- Ensure your stream source is actively producing data
|
||||
- Check network connectivity between your application and Trigger.dev
|
||||
|
||||
### Missing chunks
|
||||
|
||||
- With the current streams implementation, chunks should not be lost due to automatic resumption
|
||||
- Verify you're reading from the correct stream key
|
||||
- Check the `startIndex` option if you're not seeing expected chunks
|
||||
|
||||
### Input streams not working
|
||||
|
||||
- Input streams require SDK **4.4.2 or later** and the default streams (v2) infrastructure. Ensure you're on a recent SDK and not using the legacy metadata.stream() API.
|
||||
- If `.on()` or `.once()` throw, follow the error message to enable v2 streams (they are default in 4.1.0+).
|
||||
|
||||
### "Stream is being deleted" during long waits
|
||||
|
||||
If a stream is created but stays empty for ~1 hour (for example, during a long `wait.forToken()` or `wait.for()`), the streams backend may garbage-collect it. When the run resumes and tries to use the stream, you'll see `S2Error: Stream is being deleted` and the task retries from the beginning.
|
||||
|
||||
Two ways to avoid this:
|
||||
|
||||
- Close the stream before the wait and open a new one when the run resumes.
|
||||
- Write a heartbeat record to the stream every 20–30 minutes during the wait so it's never empty long enough to be deleted.
|
||||
Reference in New Issue
Block a user