{"slug":"trigger-dev-background-jobs","title":"Trigger.dev: Background Jobs, Scheduled Tasks, and Long-Running Workflows","tags":["trigger-dev","background-jobs","scheduling","typescript","queues","workflows"],"agent_summary":"Trigger.dev background job patterns — task definitions, scheduled cron jobs, batch processing, retry configuration, wait/sleep, concurrency limits, and integration with Next.js API routes for job triggering.","trigger_phrases":["Trigger.dev","background jobs","Trigger task","scheduled job","Trigger.dev cron","long-running task","job queue","Trigger.dev workflow"],"runnable":false,"markdown":"\n## Overview\n\nTrigger.dev runs background jobs, scheduled tasks, and long-running workflows outside of serverless function limits. Tasks can run for hours, not just seconds.\n\n## Installation\n\n```bash\nnpm install @trigger.dev/sdk\nnpx trigger.dev@latest init\n```\n\nThis creates `trigger.config.ts` and `src/trigger/` directory.\n\n## Basic Task\n\n```typescript\n// src/trigger/send-email.ts\nimport { task } from \"@trigger.dev/sdk/v3\";\n\nexport const sendEmailTask = task({\n  id: \"send-email\",\n  retry: {\n    maxAttempts: 3,\n    factor: 2,\n    minTimeoutInMs: 1_000,\n    maxTimeoutInMs: 30_000,\n  },\n  run: async (payload: { to: string; subject: string; body: string }) => {\n    const result = await emailService.send(payload);\n    return { messageId: result.id, sentAt: new Date().toISOString() };\n  },\n});\n```\n\n## Trigger from API Route\n\n```typescript\n// src/app/api/send-email/route.ts\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { sendEmailTask } from \"@/trigger/send-email\";\n\nexport async function POST(request: NextRequest) {\n  const payload = await request.json();\n\n  const handle = await sendEmailTask.trigger(payload);\n\n  return NextResponse.json({\n    jobId: handle.id,\n    status: \"queued\",\n  });\n}\n```\n\n## Scheduled Tasks (Cron)\n\n```typescript\n// src/trigger/daily-digest.ts\nimport { schedules } from \"@trigger.dev/sdk/v3\";\n\nexport const dailyDigestTask = schedules.task({\n  id: \"daily-digest\",\n  cron: \"0 9 * * 1-5\",  // 9am UTC weekdays\n  run: async () => {\n    const users = await db.from(\"users\").select().eq(\"digest_enabled\", true);\n\n    for (const user of users) {\n      await sendDigestEmail(user);\n    }\n\n    return { processed: users.length };\n  },\n});\n```\n\n## Long-Running Task with Wait\n\n```typescript\nimport { task, wait } from \"@trigger.dev/sdk/v3\";\n\nexport const importDataTask = task({\n  id: \"import-data\",\n  machine: { preset: \"large-1x\" },  // more memory for heavy tasks\n  run: async (payload: { fileUrl: string }) => {\n    // Download file\n    const data = await downloadFile(payload.fileUrl);\n\n    // Process in batches with pauses (avoid rate limits)\n    const batches = chunk(data, 100);\n    const results = [];\n\n    for (let i = 0; i < batches.length; i++) {\n      const batchResult = await processBatch(batches[i]);\n      results.push(...batchResult);\n\n      // Pause between batches\n      if (i < batches.length - 1) {\n        await wait.for({ seconds: 2 });\n      }\n    }\n\n    return { imported: results.length };\n  },\n});\n```\n\n## Batch Trigger\n\n```typescript\n// Trigger multiple jobs at once\nconst handles = await sendEmailTask.batchTrigger(\n  users.map(user => ({\n    payload: {\n      to: user.email,\n      subject: \"Weekly Report\",\n      body: generateReport(user),\n    },\n  }))\n);\n\nconsole.log(`Triggered ${handles.runs.length} jobs`);\n```\n\n## Wait for Another Task\n\n```typescript\nimport { task, runs } from \"@trigger.dev/sdk/v3\";\nimport { importDataTask } from \"./import-data\";\n\nexport const processAfterImport = task({\n  id: \"process-after-import\",\n  run: async (payload: { fileUrl: string }) => {\n    // Trigger dependent task and wait for it\n    const importHandle = await importDataTask.trigger({ fileUrl: payload.fileUrl });\n\n    // Wait for completion\n    const importResult = await runs.poll(importHandle.id, { pollIntervalMs: 5_000 });\n\n    if (importResult.status !== \"COMPLETED\") {\n      throw new Error(`Import failed: ${importResult.status}`);\n    }\n\n    // Continue with import results\n    return await processImportedData(importResult.output);\n  },\n});\n```\n\n## Concurrency Limits\n\n```typescript\nexport const apiSyncTask = task({\n  id: \"api-sync\",\n  concurrencyLimit: {\n    limit: 5,  // max 5 concurrent runs\n    key: (payload) => payload.accountId,  // per-account limit\n  },\n  run: async (payload: { accountId: string }) => {\n    // Only 5 syncs per account run simultaneously\n  },\n});\n```\n\n## Retry Configuration\n\n```typescript\nexport const webhookTask = task({\n  id: \"process-webhook\",\n  retry: {\n    maxAttempts: 5,\n    factor: 1.8,\n    minTimeoutInMs: 500,\n    maxTimeoutInMs: 60_000,\n    randomize: true,  // jitter\n  },\n  run: async (payload) => {\n    // Automatically retried with exponential backoff on throw\n    const result = await externalApi.call(payload);\n    return result;\n  },\n});\n```\n\n## Context and Metadata\n\n```typescript\nexport const myTask = task({\n  id: \"my-task\",\n  run: async (payload, { ctx }) => {\n    // Access task metadata\n    console.log(`Run ID: ${ctx.run.id}`);\n    console.log(`Attempt: ${ctx.attempt.number}`);\n    console.log(`Task ID: ${ctx.task.id}`);\n\n    // Mark progress\n    await ctx.exportTelemetry.span(\"processing\", async () => {\n      await doWork();\n    });\n  },\n});\n```\n\n## Trigger.dev Config\n\n```typescript\n// trigger.config.ts\nimport { defineConfig } from \"@trigger.dev/sdk/v3\";\n\nexport default defineConfig({\n  project: \"proj_xxx\",\n  runtime: \"node\",\n  logLevel: \"log\",\n  retries: {\n    enabledInDev: false,  // don't retry in development\n    default: {\n      maxAttempts: 3,\n      minTimeoutInMs: 1_000,\n      maxTimeoutInMs: 10_000,\n      factor: 2,\n    },\n  },\n  dirs: [\"./src/trigger\"],\n});\n```\n\n## Checking Job Status\n\n```typescript\nimport { runs } from \"@trigger.dev/sdk/v3\";\n\nconst run = await runs.retrieve(runId);\nconsole.log(run.status);  // QUEUED | EXECUTING | COMPLETED | FAILED | CANCELED\nconsole.log(run.output);  // task return value (when COMPLETED)\n```\n\n## When to Use Trigger.dev vs API Routes\n\n| Scenario | Use |\n|----------|-----|\n| < 10 second response needed | API route |\n| Email sending, webhook processing | Trigger.dev task |\n| File processing, data import | Trigger.dev task |\n| Scheduled data sync | Trigger.dev cron |\n| Multi-step workflows | Trigger.dev task with waits |\n| Real-time streaming response | API route with streaming |\n","html":"<h2>Overview</h2>\n<p>Trigger.dev runs background jobs, scheduled tasks, and long-running workflows outside of serverless function limits. Tasks can run for hours, not just seconds.</p>\n<h2>Installation</h2>\n<pre><code class=\"language-bash\">npm install @trigger.dev/sdk\nnpx trigger.dev@latest init\n</code></pre>\n<p>This creates <code>trigger.config.ts</code> and <code>src/trigger/</code> directory.</p>\n<h2>Basic Task</h2>\n<pre><code class=\"language-typescript\">// src/trigger/send-email.ts\nimport { task } from \"@trigger.dev/sdk/v3\";\n\nexport const sendEmailTask = task({\n  id: \"send-email\",\n  retry: {\n    maxAttempts: 3,\n    factor: 2,\n    minTimeoutInMs: 1_000,\n    maxTimeoutInMs: 30_000,\n  },\n  run: async (payload: { to: string; subject: string; body: string }) => {\n    const result = await emailService.send(payload);\n    return { messageId: result.id, sentAt: new Date().toISOString() };\n  },\n});\n</code></pre>\n<h2>Trigger from API Route</h2>\n<pre><code class=\"language-typescript\">// src/app/api/send-email/route.ts\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { sendEmailTask } from \"@/trigger/send-email\";\n\nexport async function POST(request: NextRequest) {\n  const payload = await request.json();\n\n  const handle = await sendEmailTask.trigger(payload);\n\n  return NextResponse.json({\n    jobId: handle.id,\n    status: \"queued\",\n  });\n}\n</code></pre>\n<h2>Scheduled Tasks (Cron)</h2>\n<pre><code class=\"language-typescript\">// src/trigger/daily-digest.ts\nimport { schedules } from \"@trigger.dev/sdk/v3\";\n\nexport const dailyDigestTask = schedules.task({\n  id: \"daily-digest\",\n  cron: \"0 9 * * 1-5\",  // 9am UTC weekdays\n  run: async () => {\n    const users = await db.from(\"users\").select().eq(\"digest_enabled\", true);\n\n    for (const user of users) {\n      await sendDigestEmail(user);\n    }\n\n    return { processed: users.length };\n  },\n});\n</code></pre>\n<h2>Long-Running Task with Wait</h2>\n<pre><code class=\"language-typescript\">import { task, wait } from \"@trigger.dev/sdk/v3\";\n\nexport const importDataTask = task({\n  id: \"import-data\",\n  machine: { preset: \"large-1x\" },  // more memory for heavy tasks\n  run: async (payload: { fileUrl: string }) => {\n    // Download file\n    const data = await downloadFile(payload.fileUrl);\n\n    // Process in batches with pauses (avoid rate limits)\n    const batches = chunk(data, 100);\n    const results = [];\n\n    for (let i = 0; i &#x3C; batches.length; i++) {\n      const batchResult = await processBatch(batches[i]);\n      results.push(...batchResult);\n\n      // Pause between batches\n      if (i &#x3C; batches.length - 1) {\n        await wait.for({ seconds: 2 });\n      }\n    }\n\n    return { imported: results.length };\n  },\n});\n</code></pre>\n<h2>Batch Trigger</h2>\n<pre><code class=\"language-typescript\">// Trigger multiple jobs at once\nconst handles = await sendEmailTask.batchTrigger(\n  users.map(user => ({\n    payload: {\n      to: user.email,\n      subject: \"Weekly Report\",\n      body: generateReport(user),\n    },\n  }))\n);\n\nconsole.log(`Triggered ${handles.runs.length} jobs`);\n</code></pre>\n<h2>Wait for Another Task</h2>\n<pre><code class=\"language-typescript\">import { task, runs } from \"@trigger.dev/sdk/v3\";\nimport { importDataTask } from \"./import-data\";\n\nexport const processAfterImport = task({\n  id: \"process-after-import\",\n  run: async (payload: { fileUrl: string }) => {\n    // Trigger dependent task and wait for it\n    const importHandle = await importDataTask.trigger({ fileUrl: payload.fileUrl });\n\n    // Wait for completion\n    const importResult = await runs.poll(importHandle.id, { pollIntervalMs: 5_000 });\n\n    if (importResult.status !== \"COMPLETED\") {\n      throw new Error(`Import failed: ${importResult.status}`);\n    }\n\n    // Continue with import results\n    return await processImportedData(importResult.output);\n  },\n});\n</code></pre>\n<h2>Concurrency Limits</h2>\n<pre><code class=\"language-typescript\">export const apiSyncTask = task({\n  id: \"api-sync\",\n  concurrencyLimit: {\n    limit: 5,  // max 5 concurrent runs\n    key: (payload) => payload.accountId,  // per-account limit\n  },\n  run: async (payload: { accountId: string }) => {\n    // Only 5 syncs per account run simultaneously\n  },\n});\n</code></pre>\n<h2>Retry Configuration</h2>\n<pre><code class=\"language-typescript\">export const webhookTask = task({\n  id: \"process-webhook\",\n  retry: {\n    maxAttempts: 5,\n    factor: 1.8,\n    minTimeoutInMs: 500,\n    maxTimeoutInMs: 60_000,\n    randomize: true,  // jitter\n  },\n  run: async (payload) => {\n    // Automatically retried with exponential backoff on throw\n    const result = await externalApi.call(payload);\n    return result;\n  },\n});\n</code></pre>\n<h2>Context and Metadata</h2>\n<pre><code class=\"language-typescript\">export const myTask = task({\n  id: \"my-task\",\n  run: async (payload, { ctx }) => {\n    // Access task metadata\n    console.log(`Run ID: ${ctx.run.id}`);\n    console.log(`Attempt: ${ctx.attempt.number}`);\n    console.log(`Task ID: ${ctx.task.id}`);\n\n    // Mark progress\n    await ctx.exportTelemetry.span(\"processing\", async () => {\n      await doWork();\n    });\n  },\n});\n</code></pre>\n<h2>Trigger.dev Config</h2>\n<pre><code class=\"language-typescript\">// trigger.config.ts\nimport { defineConfig } from \"@trigger.dev/sdk/v3\";\n\nexport default defineConfig({\n  project: \"proj_xxx\",\n  runtime: \"node\",\n  logLevel: \"log\",\n  retries: {\n    enabledInDev: false,  // don't retry in development\n    default: {\n      maxAttempts: 3,\n      minTimeoutInMs: 1_000,\n      maxTimeoutInMs: 10_000,\n      factor: 2,\n    },\n  },\n  dirs: [\"./src/trigger\"],\n});\n</code></pre>\n<h2>Checking Job Status</h2>\n<pre><code class=\"language-typescript\">import { runs } from \"@trigger.dev/sdk/v3\";\n\nconst run = await runs.retrieve(runId);\nconsole.log(run.status);  // QUEUED | EXECUTING | COMPLETED | FAILED | CANCELED\nconsole.log(run.output);  // task return value (when COMPLETED)\n</code></pre>\n<h2>When to Use Trigger.dev vs API Routes</h2>\n<p>| Scenario | Use |\n|----------|-----|\n| &#x3C; 10 second response needed | API route |\n| Email sending, webhook processing | Trigger.dev task |\n| File processing, data import | Trigger.dev task |\n| Scheduled data sync | Trigger.dev cron |\n| Multi-step workflows | Trigger.dev task with waits |\n| Real-time streaming response | API route with streaming |</p>\n"}