Inngest runs durable functions on your existing serverless infrastructure — no separate workers to provision. Functions are broken into step.run() units that each retry independently on failure. step.sleep() pauses execution without consuming function runtime. step.waitForEvent() blocks until a matching event arrives. The Inngest event bus decouples producers from consumers — one sendEvent call fans out to multiple functions. Inngest intruments Next.js API routes, Express servers, Remix loaders, and any HTTP handler with a single serve() wrapper. The Inngest Dev Server runs locally and replays events. Claude Code generates Inngest function definitions, multi-step workflows, event fan-out patterns, and the serve handlers for production Next.js and serverless deployments.
CLAUDE.md for Inngest Projects
## Inngest Stack
- Version: inngest >= 3.22, @inngest/next >= 3.22 (or @inngest/express, etc.)
- Functions: inngest.createFunction({ id, event }, async ({ event, step }) => {...})
- Steps: step.run("name", async () => {...}) — each step retries independently
- Sleep: await step.sleep("wait", "1 hour") — free pause, no runtime cost
- Events: inngest.send({ name: "order/created", data: {...} })
- Fan-out: multiple functions listen to same event name
- Dev: npx inngest-cli@latest dev — local Dev Server at localhost:8288
Core Function
// inngest/functions/process-order.ts — durable multi-step function
import { inngest } from "../client"
import { db } from "@/lib/db"
import { emailService } from "@/lib/email"
import { fulfillmentApi } from "@/lib/fulfillment"
import { inventoryApi } from "@/lib/inventory"
export const processOrder = inngest.createFunction(
{
id: "process-order",
// Retry config for entire function (each step also retries)
retries: 3,
// Concurrency: max 10 simultaneous runs of this function
concurrency: {
limit: 10,
},
},
{ event: "order/created" },
async ({ event, step }) => {
const { orderId, customerId, items, totalCents } = event.data
// Step 1: Validate inventory — retries independently on failure
const inventoryResult = await step.run("check-inventory", async () => {
for (const item of items) {
const available = await inventoryApi.checkStock(item.productId, item.quantity)
if (!available) {
throw new Error(`Product ${item.productId} out of stock`)
}
}
return { available: true }
})
// Step 2: Reserve inventory
const reservation = await step.run("reserve-inventory", async () => {
return await inventoryApi.reserve(
items.map(i => ({ productId: i.productId, quantity: i.quantity }))
)
})
// Step 3: Update order status in DB
await step.run("update-order-processing", async () => {
await db.orders.update({
where: { id: orderId },
data: { status: "processing", reservationId: reservation.id },
})
})
// Step 4: Submit to fulfillment (external API — can take 30-60s)
const fulfillment = await step.run("submit-fulfillment", async () => {
return await fulfillmentApi.submit({ orderId, customerId, items })
})
// Step 5: Final DB update
await step.run("update-order-fulfilled", async () => {
await db.orders.update({
where: { id: orderId },
data: {
status: "fulfilled",
fulfillmentId: fulfillment.id,
estimatedDelivery: fulfillment.estimatedDelivery,
},
})
})
// Step 6: Send confirmation email
await step.run("send-confirmation", async () => {
await emailService.sendOrderConfirmation({
customerId,
orderId,
estimatedDelivery: fulfillment.estimatedDelivery,
})
})
// Fan out: emit event for other functions to consume
await inngest.send({
name: "order/fulfilled",
data: {
orderId,
customerId,
totalCents,
fulfillmentId: fulfillment.id,
},
})
return { orderId, fulfillmentId: fulfillment.id }
}
)
Inngest Client and Serve Handler
// inngest/client.ts — shared client
import { Inngest } from "inngest"
// Type all events for end-to-end TypeScript safety
type Events = {
"order/created": {
data: {
orderId: string
customerId: string
items: { productId: string; quantity: number; priceCents: number }[]
totalCents: number
}
}
"order/fulfilled": {
data: {
orderId: string
customerId: string
totalCents: number
fulfillmentId: string
}
}
"order/shipped": {
data: { orderId: string; trackingNumber: string }
}
"refund/requested": {
data: { orderId: string; amountCents: number; reason: string }
user: { id: string; email: string }
}
}
export const inngest = new Inngest<Events>({ id: "my-app" })
// app/api/inngest/route.ts — Next.js App Router serve handler
import { serve } from "inngest/next"
import { inngest } from "@/inngest/client"
import { processOrder } from "@/inngest/functions/process-order"
import { sendDailyDigest } from "@/inngest/functions/scheduled"
import { processRefund } from "@/inngest/functions/refund-workflow"
import { updateLoyaltyPoints } from "@/inngest/functions/loyalty"
export const { GET, POST, PUT } = serve({
client: inngest,
functions: [
processOrder,
sendDailyDigest,
processRefund,
updateLoyaltyPoints,
],
})
Sleep and Drip Sequences
// inngest/functions/onboarding-drip.ts — timed email sequence
import { inngest } from "../client"
export const onboardingDrip = inngest.createFunction(
{ id: "onboarding-drip-sequence" },
{ event: "user/signed-up" },
async ({ event, step }) => {
const { userId, email, name } = event.data
// Day 0: Welcome email — immediate
await step.run("send-welcome", async () => {
await emailService.send({
to: email,
template: "welcome",
vars: { name },
})
})
// Step paused for 1 day — zero runtime cost
await step.sleep("wait-day-1", "1 day")
// Day 1: Tips email
await step.run("send-day-1-tips", async () => {
const user = await db.users.findUnique({ where: { id: userId } })
if (user?.hasCompletedOnboarding) return // Skip if already active
await emailService.send({
to: email,
template: "onboarding-tips",
vars: { name },
})
})
await step.sleep("wait-day-3", "2 days") // Day 3
// Day 3: Case study
await step.run("send-day-3-case-study", async () => {
await emailService.send({
to: email,
template: "case-study",
vars: { name },
})
})
await step.sleep("wait-day-7", "4 days") // Day 7
// Day 7: Upgrade offer
await step.run("send-day-7-upgrade", async () => {
const user = await db.users.findUnique({
where: { id: userId },
include: { subscription: true },
})
if (user?.subscription?.plan === "pro") return // Already upgraded
await emailService.send({
to: email,
template: "upgrade-offer",
vars: { name, discountCode: "WELCOME20" },
})
})
}
)
waitForEvent: Human-in-the-Loop
// inngest/functions/refund-workflow.ts — pause for approval
import { inngest } from "../client"
export const processRefund = inngest.createFunction(
{ id: "process-refund" },
{ event: "refund/requested" },
async ({ event, step }) => {
const { orderId, amountCents, reason } = event.data
const { id: userId } = event.user!
// Large refunds need human approval
if (amountCents > 10_000) {
// Send Slack notification with approve/reject options
await step.run("notify-support", async () => {
await slackService.notifyRefundApproval({
orderId,
amountCents,
reason,
// Deep link back to trigger approval event
approveUrl: `https://admin.example.com/refunds/${orderId}/approve`,
rejectUrl: `https://admin.example.com/refunds/${orderId}/reject`,
})
})
// Wait up to 48h for decision event
const decision = await step.waitForEvent("wait-for-approval", {
event: "refund/decision",
timeout: "48h",
// Match this specific refund
match: "data.orderId",
})
if (!decision || decision.data.action !== "approved") {
await step.run("mark-rejected", async () => {
await db.refundRequests.update({
where: { orderId },
data: { status: "rejected" },
})
await emailService.sendRefundRejected({ userId, orderId })
})
return { approved: false }
}
}
// Process refund
const refund = await step.run("process-payment-refund", async () => {
return await paymentProvider.createRefund({ orderId, amountCents })
})
await step.run("update-records", async () => {
await Promise.all([
db.orders.update({
where: { id: orderId },
data: { status: "refunded" },
}),
db.refundRequests.update({
where: { orderId },
data: { status: "completed", refundId: refund.id },
}),
])
})
await step.run("notify-customer", async () => {
await emailService.sendRefundConfirmed({ userId, amountCents, refundId: refund.id })
})
return { refundId: refund.id, amountCents }
}
)
Scheduled Functions
// inngest/functions/scheduled.ts — cron-based Inngest functions
import { inngest } from "../client"
export const sendDailyDigest = inngest.createFunction(
{ id: "send-daily-digest" },
{ cron: "0 9 * * *" }, // 9am UTC daily
async ({ step }) => {
// Get yesterday's stats
const stats = await step.run("fetch-stats", async () => {
return await db.$queryRaw`
SELECT
COUNT(*)::int as order_count,
SUM(total_cents)::bigint as revenue_cents
FROM orders
WHERE created_at >= NOW() - INTERVAL '1 day'
AND status != 'cancelled'
`
})
await step.run("send-digest-emails", async () => {
const admins = await db.users.findMany({ where: { role: "admin" } })
await Promise.all(
admins.map(admin =>
emailService.send({
to: admin.email,
template: "daily-digest",
vars: { stats },
})
)
)
})
return { sent: true }
}
)
For the Trigger.dev alternative offering similar durable background tasks with a focus on long-running jobs up to hours without serverless limitations, see the Trigger.dev guide for queue-based task patterns. For the AWS Step Functions managed state machine approach across Lambda functions with visual workflow orchestration, the serverless guide covers Lambda and Step Functions composition. The Claude Skills 360 bundle includes Inngest skill sets covering multi-step functions, scheduled jobs, and waitForEvent patterns. Start with the free tier to try Inngest workflow generation.