SOP-002: Writing Trigger.dev Tasks
FreshDocument ID: SOP-002 Version: 1.0 Status: Active Last Updated: 2026-02-21
Overview
Tasks are the core unit of work in Trigger.dev. This SOP covers task structure, configuration options, lifecycle hooks, and best practices for production-ready tasks.
Task Anatomy
Minimum Valid Task
// trigger/my-task.ts
import { task } from "@trigger.dev/sdk";
export const myTask = task({
id: "my-task", // Unique across your project
run: async (payload: { userId: string }) => {
// Your code here — runs as long as needed, no timeouts
console.log(`Processing user ${payload.userId}`);
return { processed: true };
},
});Task Configuration Options
Retry Configuration
export const myTask = task({
id: "my-task",
retry: {
maxAttempts: 5, // Default: 3
factor: 1.8, // Backoff multiplier
minTimeoutInMs: 500, // Start delay
maxTimeoutInMs: 30_000, // Max delay
randomize: false, // Add jitter
},
run: async (payload) => { /* ... */ },
});Queue / Concurrency Control
export const oneAtATime = task({
id: "one-at-a-time",
queue: {
concurrencyLimit: 1, // Only 1 run at a time
},
run: async (payload) => { /* ... */ },
});Machine Presets
export const heavyTask = task({
id: "heavy-task",
machine: {
preset: "large-1x", // 4 vCPU, 8 GB RAM
},
run: async (payload) => { /* ... */ },
});Available presets: micro, small-1x, small-2x, medium-1x, medium-2x, large-1x, large-2x
Max Duration
export const timedTask = task({
id: "timed-task",
maxDuration: 300, // 5 minutes — stops run if exceeded
run: async (payload) => { /* ... */ },
});Lifecycle Hooks
Per-Task Hooks
export const taskWithHooks = task({
id: "task-with-hooks",
// Called before EACH attempt
onStartAttempt: async ({ payload, ctx }) => {
if (ctx.run.attempt.number === 1) {
console.log("First attempt starting");
}
},
// Called once on run success (after all retries if needed)
onSuccess: async ({ payload, output, ctx }) => {
await notifySuccess(ctx.run.id, output);
},
// Called once after all retries exhausted
onFailure: async ({ payload, error, ctx }) => {
await alertTeam(error);
},
// Called on success OR failure
onComplete: async ({ output, ctx }) => {
await cleanup(ctx.run.id);
},
run: async (payload: { id: string }) => {
return { processed: payload.id };
},
});Global Hooks (init.ts)
Create trigger/init.ts for hooks that apply to ALL tasks:
// trigger/init.ts
import { tasks } from "@trigger.dev/sdk";
tasks.onStartAttempt(({ ctx, task }) => {
console.log(`[${task}] Attempt ${ctx.run.attempt.number} starting`);
});
tasks.onFailure(({ ctx, error, task }) => {
console.error(`[${task}] Failed:`, error);
// Send to Sentry, PagerDuty, Slack, etc.
});Middleware (Database Connections)
// trigger/db.ts
import { locals, tasks, logger } from "@trigger.dev/sdk";
const DbLocal = locals.create<DbClient>("db");
export function getDb() {
return locals.getOrThrow(DbLocal);
}
tasks.middleware("db", async ({ next }) => {
const db = createDbClient();
locals.set(DbLocal, db);
await db.connect();
await next();
await db.disconnect();
});
// Disconnect on wait, reconnect on resume
tasks.onWait("db", async () => { await getDb().disconnect(); });
tasks.onResume("db", async () => { await getDb().connect(); });Usage in tasks:
import { getDb } from "./db";
export const dbTask = task({
id: "db-task",
run: async (payload) => {
const db = getDb(); // Available because of middleware
const result = await db.query("SELECT * FROM users WHERE id = $1", [payload.userId]);
return result;
},
});Step-by-Step: Creating a Production Task
Step 1: Define the task file
// trigger/process-order.ts
import { task } from "@trigger.dev/sdk";
import { getDb } from "./db";
export const processOrder = task({
id: "process-order",
retry: {
maxAttempts: 3,
minTimeoutInMs: 1000,
maxTimeoutInMs: 10000,
factor: 2,
},
onFailure: async ({ error, ctx }) => {
console.error(`Order ${ctx.run.id} failed:`, error);
},
run: async (payload: { orderId: string; userId: string }) => {
const db = getDb();
// 1. Fetch order
const order = await db.query("SELECT * FROM orders WHERE id = $1", [payload.orderId]);
// 2. Process payment
const payment = await processPayment(order);
// 3. Update status
await db.query("UPDATE orders SET status = $1 WHERE id = $2", ["completed", payload.orderId]);
// 4. Send confirmation
await sendConfirmationEmail(payload.userId, order);
return { orderId: payload.orderId, status: "completed" };
},
});Step 2: Export from your trigger directory
Ensure the file is in /trigger/ — it will be auto-discovered.
Step 3: Trigger from your backend
import { tasks } from "@trigger.dev/sdk";
import type { processOrder } from "~/trigger/process-order";
// In your API route or server action:
const handle = await tasks.trigger<typeof processOrder>("process-order", {
orderId: "ord_123",
userId: "usr_456",
});
console.log("Task running:", handle.id);Verification Checklist
- [ ] Task has a unique
idstring - [ ]
runfunction is async - [ ] Return value is JSON-serializable
- [ ] Error paths throw (not swallow) errors to trigger retries
- [ ] Retry config appropriate for task type
- [ ] Sensitive operations wrapped in try/catch where needed
- [ ] Task exported from its file
Troubleshooting
Task not appearing in dashboard
Make sure the file is inside your /trigger directory (as configured in trigger.config.ts). The dev server must be restarted after adding new task files.
Retries not working
Check that your run function actually throws an error on failure. If you catch and swallow errors, the task appears successful and won't retry.
Type errors on payload
Use TypeScript interfaces for your payload type. The payload must be JSON-serializable (no functions, Dates should be ISO strings).