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.