Claude Code for Inngest: Durable Functions and Event-Driven Workflows — Claude Skills 360 Blog
Blog / Backend / Claude Code for Inngest: Durable Functions and Event-Driven Workflows
Backend

Claude Code for Inngest: Durable Functions and Event-Driven Workflows

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

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.

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