Claude Code for Trigger.dev: Background Jobs and Event-Driven Workflows — Claude Skills 360 Blog
Blog / Backend / Claude Code for Trigger.dev: Background Jobs and Event-Driven Workflows
Backend

Claude Code for Trigger.dev: Background Jobs and Event-Driven Workflows

Published: January 26, 2027
Read time: 8 min read
By: Claude Skills 360

Trigger.dev v3 runs TypeScript background tasks that can execute for minutes or hours without serverless timeout constraints. Tasks are defined with task({ id, run }) — they execute on Trigger.dev’s infrastructure, retry automatically on failure, and stream real-time logs to your dashboard. schedules.task runs on cron expressions. wait.for pauses a run until a duration elapses or an event arrives. Batch triggering fans out work across thousands of items with configurable concurrency. The trigger package integrates with any framework — call myTask.trigger(payload) from Next.js API routes, Astro endpoints, or Express handlers. Claude Code generates task definitions, event-driven workflows, scheduled jobs, and the local development patterns for production Trigger.dev applications.

CLAUDE.md for Trigger.dev Projects

## Trigger.dev Stack
- Version: @trigger.dev/sdk >= 3.3, trigger.dev CLI >= 3.3
- Tasks: export const myTask = task({ id: "my-task", run: async (payload) => {...} })
- Schedules: export const myCron = schedules.task({ id: "my-cron", cron: "0 9 * * *", run: ... })
- Trigger from app: await myTask.trigger(payload) or myTask.batchTrigger([...])
- Wait: await wait.for({ seconds: 30 }) or await wait.forEvent(...)
- Retry: retry.onThrow({ maxAttempts: 3, factor: 2, minTimeoutInMs: 1000 })
- Dev: npx trigger.dev@latest dev — runs tasks locally against cloud

Basic Task

// trigger/order-processing.ts — Trigger.dev v3 task
import { task, retry, logger } from "@trigger.dev/sdk/v3"
import { db } from "@/lib/db"
import { emailService } from "@/lib/email"
import { fulfillmentApi } from "@/lib/fulfillment"

export interface ProcessOrderPayload {
  orderId: string
  customerId: string
  totalCents: number
}

export const processOrder = task({
  id: "process-order",

  // Retry configuration
  retry: {
    maxAttempts: 3,
    factor: 2,
    minTimeoutInMs: 1_000,
    maxTimeoutInMs: 30_000,
    randomize: true,
  },

  // Task timeout (default: 30s for serverless, 10min max on Trigger.dev)
  machine: { preset: "small-1x" },

  run: async (payload: ProcessOrderPayload, { ctx }) => {
    logger.info("Processing order", { orderId: payload.orderId })

    // Update status to processing
    await db.orders.update({
      where: { id: payload.orderId },
      data: { status: "processing" },
    })

    // Call external fulfillment API (can take 30–60s)
    const fulfillment = await retry.onThrow(
      async () => {
        const result = await fulfillmentApi.submit({
          orderId: payload.orderId,
          customerId: payload.customerId,
        })
        return result
      },
      { maxAttempts: 5, factor: 1.5, minTimeoutInMs: 2_000 }
    )

    // Store fulfillment ID
    await db.orders.update({
      where: { id: payload.orderId },
      data: {
        status: "fulfilled",
        fulfillmentId: fulfillment.id,
        estimatedDelivery: fulfillment.estimatedDelivery,
      },
    })

    // Send confirmation email
    await emailService.sendOrderConfirmation({
      orderId: payload.orderId,
      customerId: payload.customerId,
      fulfillmentId: fulfillment.id,
    })

    logger.info("Order processed successfully", {
      orderId: payload.orderId,
      fulfillmentId: fulfillment.id,
    })

    return { fulfillmentId: fulfillment.id }
  },
})

Triggering from Your App

// app/api/orders/route.ts — Next.js App Router
import { processOrder } from "@/trigger/order-processing"
import { sendOrderDigest } from "@/trigger/email-digest"

export async function POST(request: Request) {
  const body = await request.json()

  const order = await db.orders.create({
    data: {
      customerId: body.customerId,
      items: body.items,
      status: "pending",
      totalCents: calculateTotal(body.items),
    },
  })

  // Fire-and-forget: non-blocking background task
  const handle = await processOrder.trigger({
    orderId: order.id,
    customerId: order.customerId,
    totalCents: order.totalCents,
  })

  // Store run ID for status polling
  await db.orders.update({
    where: { id: order.id },
    data: { triggerRunId: handle.id },
  })

  return Response.json({ orderId: order.id, runId: handle.id })
}

// Check run status
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const runId = searchParams.get("runId")!

  const run = await processOrder.retrieve(runId)

  return Response.json({
    status: run.status,
    output: run.output,
    startedAt: run.startedAt,
    finishedAt: run.finishedAt,
  })
}

Scheduled Tasks

// trigger/scheduled-jobs.ts — cron-based tasks
import { schedules, logger } from "@trigger.dev/sdk/v3"
import { db } from "@/lib/db"
import { slackService } from "@/lib/slack"

// Daily revenue digest at 9am UTC
export const dailyRevenueReport = schedules.task({
  id: "daily-revenue-report",
  cron: "0 9 * * *",

  run: async (payload) => {
    const yesterday = new Date()
    yesterday.setDate(yesterday.getDate() - 1)
    yesterday.setHours(0, 0, 0, 0)

    const today = new Date()
    today.setHours(0, 0, 0, 0)

    const [stats] = await db.$queryRaw<[RevenueStats]>`
      SELECT
        COUNT(*)::int as order_count,
        SUM(total_cents)::bigint as revenue_cents,
        AVG(total_cents)::int as avg_order_cents
      FROM orders
      WHERE created_at >= ${yesterday} AND created_at < ${today}
        AND status NOT IN ('cancelled', 'refunded')
    `

    await slackService.postToChannel("#revenue", {
      text: `📊 Yesterday's Revenue`,
      blocks: [
        {
          type: "section",
          fields: [
            { type: "mrkdwn", text: `*Orders:* ${stats.order_count}` },
            { type: "mrkdwn", text: `*Revenue:* $${(stats.revenue_cents / 100).toFixed(2)}` },
            { type: "mrkdwn", text: `*Avg Order:* $${(stats.avg_order_cents / 100).toFixed(2)}` },
          ],
        },
      ],
    })

    logger.info("Daily report sent", { stats })
    return stats
  },
})

// Hourly: clean up abandoned checkouts
export const cleanupAbandonedCheckouts = schedules.task({
  id: "cleanup-abandoned-checkouts",
  cron: "0 * * * *",

  run: async () => {
    const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000)

    const result = await db.checkouts.deleteMany({
      where: {
        status: "pending",
        createdAt: { lt: cutoff },
      },
    })

    logger.info("Cleaned up abandoned checkouts", { deleted: result.count })
    return { deleted: result.count }
  },
})

Batch Processing

// trigger/batch-tasks.ts — fan out across many items
import { task, batch } from "@trigger.dev/sdk/v3"
import { sendOrderShippedEmail } from "./email-tasks"

interface BulkShipmentPayload {
  shipmentId: string
  orderIds: string[]
}

export const processBulkShipment = task({
  id: "process-bulk-shipment",

  run: async (payload: BulkShipmentPayload) => {
    const { orderIds, shipmentId } = payload

    logger.info(`Processing shipment ${shipmentId}`, {
      orderCount: orderIds.length,
    })

    // Update all orders to shipped in DB
    await db.orders.updateMany({
      where: { id: { in: orderIds } },
      data: { status: "shipped", shipmentId },
    })

    // Fan out email notifications — batchTrigger with concurrency
    const results = await sendOrderShippedEmail.batchTrigger(
      orderIds.map(orderId => ({
        payload: { orderId, shipmentId },
      })),
    )

    const succeeded = results.runs.filter(r => r.ok).length
    const failed = results.runs.filter(r => !r.ok).length

    logger.info("Bulk shipment notifications queued", { succeeded, failed })

    return { succeeded, failed, shipmentId }
  },
})

// Individual task triggered in batch
export const sendOrderShippedEmail = task({
  id: "send-shipped-email",
  retry: { maxAttempts: 3 },

  run: async ({ orderId, shipmentId }: { orderId: string; shipmentId: string }) => {
    const order = await db.orders.findUnique({
      where: { id: orderId },
      include: { customer: true },
    })

    if (!order) throw new Error(`Order ${orderId} not found`)

    await emailService.sendShipped({
      to: order.customer.email,
      orderId,
      trackingUrl: `https://track.shipping.com/${shipmentId}`,
    })
  },
})

Human-in-the-Loop with waitForEvent

// trigger/approval-workflow.ts — pause for human approval
import { task, wait, logger } from "@trigger.dev/sdk/v3"

interface RefundRequestPayload {
  orderId: string
  customerId: string
  amountCents: number
  reason: string
}

export const processRefundRequest = task({
  id: "process-refund-request",

  run: async (payload: RefundRequestPayload, { ctx }) => {
    const { orderId, amountCents, customerId, reason } = payload

    // Large refunds need manual approval
    if (amountCents > 10_000) {
      logger.info("Large refund requires approval", { amountCents })

      // Notify support team
      await slackService.postToChannel("#refund-approvals", {
        text: `Refund approval needed: $${(amountCents / 100).toFixed(2)} for order ${orderId}`,
        blocks: [
          {
            type: "actions",
            elements: [
              {
                type: "button",
                text: { type: "plain_text", text: "Approve" },
                action_id: "approve_refund",
                // Embed run ID so Slack handler can send event
                value: JSON.stringify({ runId: ctx.run.id, action: "approve" }),
              },
              {
                type: "button",
                text: { type: "plain_text", text: "Reject" },
                action_id: "reject_refund",
                value: JSON.stringify({ runId: ctx.run.id, action: "reject" }),
                style: "danger",
              },
            ],
          },
        ],
      })

      // Wait up to 24h for approval event
      const approvalEvent = await wait.forEvent("refund-decision", {
        timeout: { hours: 24 },
        filter: { runId: ctx.run.id },
      })

      if (!approvalEvent || approvalEvent.action !== "approve") {
        logger.info("Refund rejected or timed out")
        await db.refundRequests.update({
          where: { orderId },
          data: { status: "rejected" },
        })
        return { approved: false }
      }
    }

    // Process refund via payment provider
    await paymentProvider.refund({ orderId, amountCents })

    await db.orders.update({
      where: { id: orderId },
      data: { status: "refunded", refundedAt: new Date() },
    })

    await emailService.sendRefundConfirmation({ customerId, amountCents })

    return { approved: true, refundedCents: amountCents }
  },
})

For the Inngest alternative that uses a similar event-driven model with function steps and sleeps on serverless, see the cloud functions patterns. For the BullMQ Redis-backed job queue that gives more control over queue internals with worker processes, the queue patterns guide covers BullMQ workers and flow jobs. The Claude Skills 360 bundle includes Trigger.dev skill sets covering task definitions, scheduled jobs, and batch workflows. Start with the free tier to try background job generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free