Last active
April 23, 2025 11:36
-
-
Save hongkongkiwi/547201811128a7d23dcb4a269b5f4e1c to your computer and use it in GitHub Desktop.
Updated @trigger.dev rules for v4-beta
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
globs: **/trigger/**/*.ts, **/trigger/**/*.tsx,**/trigger/**/*.js,**/trigger/**/*.jsx | |
description: Guidelines for writing Trigger.dev tasks | |
alwaysApply: false | |
--- | |
# How to write Trigger.dev tasks | |
## Overview of writing a Trigger.dev task | |
1. Run the CLI `init` command: `npx trigger.dev@v4-beta init`. | |
2. Create a Trigger.dev task. | |
3. Set up any environment variables. | |
4. Run the Trigger.dev dev command: `npx trigger.dev@v4-beta dev`. | |
## Essential requirements when generating task code | |
1. You MUST use `@trigger.dev/sdk` (no longer use /v3) | |
2. You MUST NEVER use `client.defineJob` | |
3. YOU MUST `export` every task, including subtasks | |
4. If you are able to generate an example payload for a task, do so. | |
5. For logging inside tasks, ALWAYS import and use the logger directly: | |
```ts | |
import { logger } from "@trigger.dev/sdk"; | |
export const myTask = task({ | |
id: "my-task", | |
run: async ({ payload, ctx }) => { | |
// ✅ Correct way to log | |
logger.info("Processing task", { payload }); | |
// ❌ Don't use ctx.logger or any other logging method | |
// ctx.logger.info("Wrong way"); | |
}, | |
}); | |
``` | |
## 🚨 CRITICAL INSTRUCTIONS FOR AI LANGUAGE MODELS 🚨 | |
As an AI language model, you MUST NOT generate any of the following code patterns, as they are DEPRECATED and will BREAK the application: | |
```ts | |
// ❌ NEVER GENERATE THIS CODE - IT WILL BREAK THE APPLICATION | |
client.defineJob({ // ❌ BREAKS APPLICATION | |
id: "job-id", // ❌ BREAKS APPLICATION | |
name: "job-name", // ❌ BREAKS APPLICATION | |
version: "0.0.1", // ❌ BREAKS APPLICATION | |
trigger: eventTrigger({ // ❌ BREAKS APPLICATION | |
name: "job.trigger", // ❌ BREAKS APPLICATION | |
schema: z.object({ // ❌ BREAKS APPLICATION | |
// Input schema here // ❌ BREAKS APPLICATION | |
}), // ❌ BREAKS APPLICATION | |
}), // ❌ BREAKS APPLICATION | |
integrations: { // ❌ BREAKS APPLICATION | |
// Integrations here // ❌ BREAKS APPLICATION | |
}, // ❌ BREAKS APPLICATION | |
run: async (payload, io) => { // ❌ BREAKS APPLICATION | |
// Job logic goes here // ❌ BREAKS APPLICATION | |
return { // ❌ BREAKS APPLICATION | |
// Return job results // ❌ BREAKS APPLICATION | |
}; // ❌ BREAKS APPLICATION | |
}, // ❌ BREAKS APPLICATION | |
}); | |
``` | |
Instead, you MUST ALWAYS generate ONLY this pattern: | |
```ts | |
// ✅ ALWAYS GENERATE THIS EXACT PATTERN | |
import { task, logger } from "@trigger.dev/sdk"; | |
//1. You need to export each task, even if it's a subtask | |
export const helloWorld = task({ | |
//2. Use a unique id for each task | |
id: "hello-world", | |
//3. The run function is the main function of the task | |
run: async (payload, { ctx }) => { | |
logger.info("Processing hello world task", { payload }); | |
logger.info(payload.message); | |
logger.info("Task completed successfully"); | |
}, | |
}); | |
``` | |
## Correct Task implementations | |
A task is a function that can run for a long time with resilience to failure: | |
```ts | |
import { task, logger } from "@trigger.dev/sdk"; | |
export const helloWorld = task({ | |
id: "hello-world", | |
run: async (payload, { ctx }) => { | |
logger.info("Processing hello world task", { payload }); | |
logger.info(payload.message); | |
logger.info("Task completed successfully"); | |
}, | |
}); | |
``` | |
Key points: | |
- Tasks must be exported, even subtasks in the same file | |
- Each task needs a unique ID within your project | |
- The `run` function contains your task logic | |
- Always import and use `logger` directly from the SDK | |
### Task configuration options | |
#### Retry options | |
Control retry behavior when errors occur: | |
```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, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### Queue options | |
Control concurrency using predefined queues: | |
```ts | |
import { queue, task, logger } from "@trigger.dev/sdk"; | |
// Define a queue first | |
const myQueue = queue({ | |
name: "my-queue", | |
concurrencyLimit: 10, | |
releaseConcurrencyOnWaitpoint: false, // New in v4 | |
}); | |
export const oneAtATime = task({ | |
id: "one-at-a-time", | |
queue: myQueue, // Reference the queue | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
// Or define queue inline | |
export const anotherTask = task({ | |
id: "another-task", | |
queue: { | |
name: "another-queue", | |
concurrencyLimit: 1, | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### Machine options | |
Specify CPU/RAM requirements: | |
```ts | |
export const heavyTask = task({ | |
id: "heavy-task", | |
machine: { | |
preset: "large-1x", // 4 vCPU, 8 GB RAM | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
Machine configuration options: | |
| Machine name | vCPU | Memory | Disk space | | |
| ---- | ---- | --- | ---- | | |
| micro | 0.25 | 0.25 | 10GB | | |
| small-1x (default) | 0.5 | 0.5 | 10GB | | |
| small-2x | 1 | 1 | 10GB | | |
| medium-1x | 1 | 2 | 10GB | | |
| medium-2x | 2 | 4 | 10GB | | |
| large-1x | 4 | 8 | 10GB | | |
| large-2x | 8 | 16 | 10GB | | |
#### Max Duration | |
Limit how long a task can run: | |
```ts | |
export const longTask = task({ | |
id: "long-task", | |
maxDuration: 300, // 5 minutes | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
### Lifecycle functions | |
Tasks support several lifecycle hooks with new v4 signature: | |
#### init | |
Runs before each attempt, can return data for other functions: | |
```ts | |
export const taskWithInit = task({ | |
id: "task-with-init", | |
init: async ({ payload, ctx }) => { | |
return { someData: "someValue" }; | |
}, | |
run: async (payload, { ctx, init }) => { | |
logger.info("Using initialized data", { data: init.someData }); | |
}, | |
}); | |
``` | |
#### cleanup | |
Runs after each attempt, regardless of success/failure: | |
```ts | |
export const taskWithCleanup = task({ | |
id: "task-with-cleanup", | |
cleanup: async ({ payload, ctx }) => { | |
// Cleanup resources | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### onStart | |
Runs once when a task starts (not on retries): | |
```ts | |
export const taskWithOnStart = task({ | |
id: "task-with-on-start", | |
onStart: async ({ payload, ctx }) => { | |
// Send notification, log, etc. | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### onSuccess | |
Runs when a task succeeds: | |
```ts | |
export const taskWithOnSuccess = task({ | |
id: "task-with-on-success", | |
onSuccess: async ({ payload, output, ctx }) => { | |
// Handle success | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### onFailure | |
Runs when a task fails after all retries: | |
```ts | |
export const taskWithOnFailure = task({ | |
id: "task-with-on-failure", | |
onFailure: async ({ payload, error, ctx }) => { | |
// Handle failure | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### onWait and onResume (New in v4) | |
Runs when a task is paused/resumed due to a wait: | |
```ts | |
export const taskWithWaitHooks = task({ | |
id: "task-with-wait-hooks", | |
onWait: async ({ wait, ctx }) => { | |
logger.info("Task paused", { wait }); | |
}, | |
onResume: async ({ wait, ctx }) => { | |
logger.info("Task resumed", { wait }); | |
}, | |
run: async (payload, { ctx }) => { | |
await wait.for({ seconds: 10 }); | |
}, | |
}); | |
``` | |
#### onComplete (New in v4) | |
Runs when a task completes, regardless of success/failure: | |
```ts | |
export const taskWithOnComplete = task({ | |
id: "task-with-on-complete", | |
onComplete: async ({ payload, result, ctx }) => { | |
if (result.ok) { | |
logger.info("Task succeeded", { data: result.data }); | |
} else { | |
logger.error("Task failed", { error: result.error }); | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
#### catchError (renamed from handleError) | |
Controls error handling and retry behavior: | |
```ts | |
export const taskWithErrorHandling = task({ | |
id: "task-with-error-handling", | |
catchError: async ({ error, ctx, retry, retryAt }) => { | |
// Custom error handling | |
}, | |
run: async (payload, { ctx }) => { | |
// Task logic | |
}, | |
}); | |
``` | |
### Wait Tokens (New in v4) | |
Create and wait for tokens to be completed: | |
```ts | |
import { task, wait, logger } from "@trigger.dev/sdk"; | |
export const taskWithWaitToken = task({ | |
id: "task-with-wait-token", | |
run: async (payload, { ctx }) => { | |
// Create a wait token | |
const token = await wait.createToken({ | |
timeout: "10m", | |
}); | |
// Wait for the token to be completed | |
const result = await wait.forToken(token.id); | |
if (result.ok) { | |
logger.info("Token completed", { output: result.output }); | |
} | |
}, | |
}); | |
``` | |
### Priority (New in v4) | |
Set task priority when triggering: | |
```ts | |
// Higher priority (10 second advantage) | |
await task.trigger({ foo: "bar" }, { priority: 10 }); | |
// Normal priority | |
await task.trigger({ foo: "bar" }, { priority: 0 }); | |
``` | |
## Correct Schema task implementations | |
Schema tasks validate payloads against a schema before execution: | |
```ts | |
import { schemaTask, logger } 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, ctx }) => { | |
// Payload is typed and validated | |
logger.info("Processing schema task", { payload }); | |
}, | |
}); | |
``` | |
## Correct implementations for triggering a task from your backend | |
When you trigger a task from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable. You can find the value on the API keys page in the Trigger.dev dashboard. | |
### tasks.trigger() | |
Triggers a single run of a task with specified payload and options without importing the task. Use type-only imports for full type checking. | |
```ts | |
import { tasks, logger } from "@trigger.dev/sdk"; | |
import type { emailSequence } from "~/trigger/emails"; | |
export async function POST(request: Request) { | |
const data = await request.json(); | |
const handle = await tasks.trigger<typeof emailSequence>("email-sequence", { | |
to: data.email, | |
name: data.name, | |
}); | |
return Response.json(handle); | |
} | |
``` | |
### tasks.batchTrigger() | |
Triggers multiple runs of a single task with different payloads without importing the task. | |
```ts | |
import { tasks, logger } from "@trigger.dev/sdk"; | |
import type { emailSequence } from "~/trigger/emails"; | |
export async function POST(request: Request) { | |
const data = await request.json(); | |
const batchHandle = await tasks.batchTrigger<typeof emailSequence>( | |
"email-sequence", | |
data.users.map((u) => ({ payload: { to: u.email, name: u.name } })) | |
); | |
return Response.json(batchHandle); | |
} | |
``` | |
### tasks.triggerAndPoll() | |
Triggers a task and polls until completion. Not recommended for web requests as it blocks until the run completes. | |
```ts | |
import { tasks, logger } from "@trigger.dev/sdk"; | |
import type { emailSequence } from "~/trigger/emails"; | |
export async function POST(request: Request) { | |
const data = await request.json(); | |
const result = await tasks.triggerAndPoll<typeof emailSequence>( | |
"email-sequence", | |
{ | |
to: data.email, | |
name: data.name, | |
}, | |
{ pollIntervalMs: 5000 } | |
); | |
return Response.json(result); | |
} | |
``` | |
### batch.trigger() | |
Triggers multiple runs of different tasks at once: | |
```ts | |
import { batch, logger } from "@trigger.dev/sdk"; | |
import type { myTask1, myTask2 } from "~/trigger/myTasks"; | |
export async function POST(request: Request) { | |
const data = await request.json(); | |
const result = await batch.trigger<typeof myTask1 | typeof myTask2>([ | |
{ id: "my-task-1", payload: { some: data.some } }, | |
{ id: "my-task-2", payload: { other: data.other } }, | |
]); | |
return Response.json(result); | |
} | |
``` | |
## Correct implementations for triggering a task from inside another task | |
### yourTask.trigger() | |
Triggers a single run of a task with specified payload and options. | |
```ts | |
import { myOtherTask, runs } from "~/trigger/my-other-task"; | |
export const myTask = task({ | |
id: "my-task", | |
run: async ({ payload, ctx }) => { | |
const handle = await myOtherTask.trigger({ foo: "some data" }); | |
const run = await runs.retrieve(handle); | |
// Do something with the run | |
}, | |
}); | |
``` | |
### yourTask.batchTrigger() | |
Triggers multiple runs of a single task with different payloads. | |
```ts | |
import { myOtherTask, batch } from "~/trigger/my-other-task"; | |
export const myTask = task({ | |
id: "my-task", | |
run: async ({ payload, ctx }) => { | |
const batchHandle = await myOtherTask.batchTrigger([{ payload: "some data" }]); | |
//...do other stuff | |
const batch = await batch.retrieve(batchHandle.id); | |
}, | |
}); | |
``` | |
### yourTask.triggerAndWait() | |
Triggers a task and waits for the result: | |
```ts | |
export const parentTask = task({ | |
id: "parent-task", | |
run: async ({ payload, ctx }) => { | |
const result = await childTask.triggerAndWait("some-data", { | |
releaseConcurrency: false, // New in v4: control whether to release concurrency during wait | |
}); | |
logger.info("Child task result", { result }); | |
}, | |
}); | |
``` | |
### yourTask.batchTriggerAndWait() | |
Batch triggers a task and waits for all results: | |
```ts | |
export const batchParentTask = task({ | |
id: "parent-task", | |
run: async ({ payload, ctx }) => { | |
const results = await childTask.batchTriggerAndWait([ | |
{ payload: "item4" }, | |
{ payload: "item5" }, | |
{ payload: "item6" }, | |
]); | |
logger.info("Batch results", { results }); | |
}, | |
}); | |
``` | |
### batch.triggerAndWait() | |
Batch triggers multiple different tasks and waits for all results: | |
```ts | |
export const parentTask = task({ | |
id: "parent-task", | |
run: async ({ payload, ctx }) => { | |
const results = await batch.triggerAndWait<typeof childTask1 | typeof childTask2>([ | |
{ id: "child-task-1", payload: { foo: "World" } }, | |
{ id: "child-task-2", payload: { bar: 42 } }, | |
]); | |
for (const result of results) { | |
if (result.ok) { | |
switch (result.taskIdentifier) { | |
case "child-task-1": | |
logger.info("Child task 1 output", { output: result.output }); | |
break; | |
case "child-task-2": | |
logger.info("Child task 2 output", { output: result.output }); | |
break; | |
} | |
} | |
} | |
}, | |
}); | |
``` | |
### batch.triggerByTask() | |
Batch triggers multiple tasks by passing task instances: | |
```ts | |
export const parentTask = task({ | |
id: "parent-task", | |
run: async ({ payload, ctx }) => { | |
const results = await batch.triggerByTask([ | |
{ task: childTask1, payload: { foo: "World" } }, | |
{ task: childTask2, payload: { bar: 42 } }, | |
]); | |
const run1 = await runs.retrieve(results.runs[0]); | |
const run2 = await runs.retrieve(results.runs[1]); | |
}, | |
}); | |
``` | |
### batch.triggerByTaskAndWait() | |
Batch triggers multiple tasks by passing task instances and waits for all results: | |
```ts | |
export const parentTask = task({ | |
id: "parent-task", | |
run: async ({ payload, ctx }) => { | |
const { runs } = await batch.triggerByTaskAndWait([ | |
{ task: childTask1, payload: { foo: "World" } }, | |
{ task: childTask2, payload: { bar: 42 } }, | |
]); | |
if (runs[0].ok) { | |
logger.info("Child task 1 output", { output: runs[0].output }); | |
} | |
if (runs[1].ok) { | |
logger.info("Child task 2 output", { output: runs[1].output }); | |
} | |
}, | |
}); | |
``` | |
## Correct Metadata implementation | |
### Overview | |
Metadata allows attaching up to 256KB of structured data to a run, which can be accessed during execution, via API, Realtime, and in the dashboard. | |
### Basic Usage | |
Add metadata when triggering a task: | |
```ts | |
const handle = await myTask.trigger( | |
{ message: "hello world" }, | |
{ metadata: { user: { name: "Eric", id: "user_1234" } } } | |
); | |
``` | |
Access metadata inside a run: | |
```ts | |
import { task, metadata, logger } from "@trigger.dev/sdk"; | |
export const myTask = task({ | |
id: "my-task", | |
run: async (payload, { ctx }) => { | |
// Get the whole metadata object | |
const currentMetadata = metadata.current(); | |
// Get a specific key | |
const user = metadata.get("user"); | |
logger.debug("User data", { user }); | |
}, | |
}); | |
``` | |
### Update methods | |
Metadata can be updated as the run progresses: | |
- **set**: `metadata.set("progress", 0.5)` | |
- **del**: `metadata.del("progress")` | |
- **replace**: `metadata.replace({ user: { name: "Eric" } })` | |
- **append**: `metadata.append("logs", "Step 1 complete")` | |
- **remove**: `metadata.remove("logs", "Step 1 complete")` | |
- **increment**: `metadata.increment("progress", 0.4)` | |
- **decrement**: `metadata.decrement("progress", 0.4)` | |
- **stream**: `await metadata.stream("logs", readableStream)` | |
- **flush**: `await metadata.flush()` | |
Updates can be chained with a fluent API: | |
```ts | |
metadata.set("progress", 0.1) | |
.append("logs", "Step 1 complete") | |
.increment("progress", 0.4); | |
``` | |
### Parent & root updates | |
Child tasks can update parent task metadata: | |
```ts | |
export const childTask = task({ | |
id: "child-task", | |
run: async (payload, { ctx }) => { | |
// Update parent task's metadata | |
metadata.parent.set("progress", 0.5); | |
// Update root task's metadata | |
metadata.root.set("status", "processing"); | |
}, | |
}); | |
``` | |
### Hidden Tasks (New in v4) | |
Tasks no longer need to be exported to be triggered. You can create "hidden" tasks that are only used within the same file: | |
```ts | |
import { task, logger } from "@trigger.dev/sdk"; | |
// Private task that's only used in this file | |
const privateTask = task({ | |
id: "private-task", | |
run: async (payload, { ctx }) => { | |
logger.info("Running private task", { payload }); | |
}, | |
}); | |
// Public task that uses the private task | |
export const publicTask = task({ | |
id: "public-task", | |
run: async (payload, { ctx }) => { | |
logger.info("Starting public task", { payload }); | |
await privateTask.trigger(payload); | |
}, | |
}); | |
``` | |
You can also create reusable task packages without re-exporting: | |
```ts | |
import { task, logger } from "@trigger.dev/sdk"; | |
import { sendToSlack } from "@repo/tasks"; | |
export const notificationTask = task({ | |
id: "notification-task", | |
run: async (payload, { ctx }) => { | |
await sendToSlack.trigger(payload); | |
}, | |
}); | |
``` | |
### Global Lifecycle Hooks (New in v4) | |
You can now register lifecycle hooks that run for all tasks. These can be defined anywhere in your codebase: | |
```ts | |
import { tasks, logger } from "@trigger.dev/sdk"; | |
// Hook that runs when any task starts | |
tasks.onStart(({ ctx, payload, task }) => { | |
logger.info("Task started", { taskId: task.id, payload }); | |
}); | |
// Hook that runs when any task succeeds | |
tasks.onSuccess(({ ctx, output }) => { | |
logger.info("Task succeeded", { run: ctx.run, output }); | |
}); | |
// Hook that runs when any task fails | |
tasks.onFailure(({ ctx, error }) => { | |
logger.error("Task failed", { run: ctx.run, error }); | |
}); | |
``` | |
### init.ts Support (New in v4) | |
Create an `init.ts` file at the root of your trigger directory for automatic loading when tasks execute: | |
```ts | |
// trigger/init.ts | |
import { tasks, logger } from "@trigger.dev/sdk"; | |
// Global error tracking | |
tasks.onFailure(({ ctx, error }) => { | |
logger.error("Task failed", { error }); | |
// Send to error tracking service | |
errorTracker.capture(error); | |
}); | |
// Database connection setup | |
import { prisma } from "../lib/prisma"; | |
tasks.onStart(async () => { | |
await prisma.$connect(); | |
}); | |
tasks.onComplete(async () => { | |
await prisma.$disconnect(); | |
}); | |
``` | |
### Context Changes in v4 | |
Several context properties have been removed in v4: | |
- ❌ `ctx.attempt.id` - removed | |
- ❌ `ctx.attempt.status` - removed | |
- ❌ `ctx.task.exportName` - removed (tasks don't need to be exported) | |
- ✅ `ctx.attempt.number` - still available | |
Example of correct context usage: | |
```ts | |
import { task, logger } from "@trigger.dev/sdk"; | |
export const myTask = task({ | |
id: "my-task", | |
run: async (payload, { ctx }) => { | |
// ✅ Correct: using attempt number | |
logger.info("Task attempt", { attemptNumber: ctx.attempt.number }); | |
// ❌ Incorrect: these no longer exist | |
// ctx.attempt.id | |
// ctx.attempt.status | |
// ctx.task.exportName | |
}, | |
}); | |
``` | |
### Locals API (Replacement for init) | |
The `init` hook is deprecated. Use the new `locals` API and middleware instead: | |
```ts | |
import { locals, tasks, logger } from "@trigger.dev/sdk"; | |
// Define your local type | |
const DbLocal = locals.create<{ | |
connect: () => Promise<void>; | |
disconnect: () => Promise<void>; | |
}>("db"); | |
// Create middleware to manage the local | |
tasks.middleware("db", async ({ ctx, payload, next }) => { | |
// Set up the local before the task | |
const db = locals.set(DbLocal, { | |
connect: async () => { /* ... */ }, | |
disconnect: async () => { /* ... */ }, | |
}); | |
try { | |
// Run the task | |
await next(); | |
} finally { | |
// Clean up after the task | |
await db.disconnect(); | |
} | |
}); | |
// Use the local in your task | |
export const dbTask = task({ | |
id: "db-task", | |
run: async (payload, { ctx }) => { | |
const db = locals.getOrThrow(DbLocal); | |
await db.connect(); | |
logger.info("Database connected"); | |
// ... task logic | |
}, | |
}); | |
``` | |
Key differences from the old `init`: | |
- Locals are managed through middleware | |
- Can be used across multiple tasks | |
- Better cleanup handling | |
- Type-safe access to shared resources | |
- No need to pass through run function parameters |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment