Skip to content

SOP-002: Writing Trigger.dev Tasks

Fresh

Document 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

ts
// 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

ts
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

ts
export const oneAtATime = task({
  id: "one-at-a-time",
  queue: {
    concurrencyLimit: 1,   // Only 1 run at a time
  },
  run: async (payload) => { /* ... */ },
});

Machine Presets

ts
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

ts
export const timedTask = task({
  id: "timed-task",
  maxDuration: 300,  // 5 minutes — stops run if exceeded
  run: async (payload) => { /* ... */ },
});

Lifecycle Hooks

Per-Task Hooks

ts
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:

ts
// 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)

ts
// 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:

ts
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

ts
// 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

ts
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 id string
  • [ ] run function 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).

See Also

Built from official Trigger.dev documentation