Skip to content

Instantly share code, notes, and snippets.

@hongkongkiwi
Last active April 23, 2025 11:36
Show Gist options
  • Save hongkongkiwi/547201811128a7d23dcb4a269b5f4e1c to your computer and use it in GitHub Desktop.
Save hongkongkiwi/547201811128a7d23dcb4a269b5f4e1c to your computer and use it in GitHub Desktop.
Updated @trigger.dev rules for v4-beta
---
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