chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
+697
View File
@@ -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. Its 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
![Lifecycle functions](/images/lifecycle-functions.png)
### `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` doesnt 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>
+382
View File
@@ -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. ![Blank schedules
page](/images/schedules-blank.png)
</Step>
<Step title="Create your schedule">
Fill in the form and press "Create schedule" when you're done. ![Environment variables
page](/images/schedules-create.png)
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) ![Test page](/images/schedules-test.png)
</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 ![Schedule test form](/images/schedules-test-form.png)
</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.
+413
View File
@@ -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 SDKs [`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);
},
});
```
+884
View File
@@ -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 2030 minutes during the wait so it's never empty long enough to be deleted.