Build This Now
Build This Now
Qu'est-ce que le code Claude ?Installer Claude CodeL'installateur natif de Claude CodeTon premier projet Claude Code
Claude Code v2.1.122 Release NotesClaude Code Dynamic Workflows : comment orchestrer 1 000 sous-agents sur une vraie codebaseBonnes pratiques Claude CodeMeilleures pratiques pour Claude Opus 4.7Claude Code sur un VPSIntégration GitRevue de code avec Claude CodeLes Worktrees avec Claude CodeClaude Code à distanceClaude Code ChannelsChannels, Routines, Teleport, DispatchTâches planifiées avec Claude CodePermissions Claude CodeLe mode auto de Claude CodeAjouter les paiements Stripe avec Claude CodeFeedback LoopsWorkflows TodoGestion des tâches dans Claude CodeTemplates de projetTarification et utilisation des tokens Claude CodeTarifs de Claude Code : ce que tu vas vraiment payerClaude Code Ultra ReviewConstruire une app Next.js avec Claude CodeClaude Code With Supabase: Database, Auth, RLSVercel deepsec with Claude CodeTest-Driven Development with Claude CodeConstruire un MVP SaaS avec Claude CodeAjouter l'authentification avec Claude Code (Supabase Auth)Ajouter les emails transactionnels avec Claude Code (Resend + React Email)Construire une API type-safe avec Claude Code (oRPC + Zod)How to Add File Uploads With Claude Code (Supabase Storage)How to Run Background Jobs with Claude Code and InngestHow to Build an Admin Dashboard With Claude CodeHow to Add Full-Text Search With Claude Code (Postgres)Commerce agentique : comment construire une app que les agents IA peuvent payerClaude Code 1M Context in Practice: When Bigger Isn't BetterClaude Code GitHub Actions Setup Guide (@claude + Cron)Claude Code Headless Mode: The Definitive Guide to claude -pClaude Code Max Plan vs API Cost: Break-Even GuideClaude Code Prompt Caching: The Token Discount Most People Never Turn OnCombien coûte la création d'un SaaS avec Claude Code en 2026Run a Team of AI Agents in Parallel with Git WorktreesPrompt Injection in Coding Agents: How to Not Get Pwned
speedy_devvkoen_salo
Blog/Handbook/Workflow/How to Run Background Jobs with Claude Code and Inngest

How to Run Background Jobs with Claude Code and Inngest

Move slow work off the request path. Build a durable Inngest job with retries, steps, and concurrency, triggered straight from your oRPC endpoints in Next.js 16.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →
speedy_devvWritten by speedy_devvPublished Jul 14, 202610 min readHandbook hubWorkflow index

Problem: Your API endpoint sends an email, resizes an image, and calls two webhooks before it returns. The user waits 6 seconds for a spinner, and if the image service times out, the whole request 500s and nothing gets retried.

Fix: Move that work off the request path. Your endpoint sends one event and returns in milliseconds. A durable Inngest function picks it up, runs each piece as a retriable step, caps its own concurrency, and survives crashes. This guide builds exactly that in Next.js 16, wired to your oRPC endpoints, with Claude Code doing the typing.


Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

What "Durable" Actually Buys You

A normal background job is fire-and-forget. You push it onto a queue, a worker picks it up, and if the worker dies mid-job you either lose the work or replay the whole thing from the top (sending duplicate emails).

Inngest runs your function as a series of steps, and every step is a checkpoint. Once a step finishes, its result is saved. If the function crashes or a later step fails, Inngest re-invokes your function, replays the saved results of the steps that already completed, and resumes at the failed one. You get exactly-once semantics per step without writing any of the bookkeeping yourself.

You write ordinary async TypeScript, wrap the risky parts in step.run(...), and Inngest turns it into a crash-safe workflow with retries and concurrency control baked in. Three jobs show this off, and we will build all three: an email send, an image resize, and a webhook fan-out.

Install and Point Claude at the Docs

Start in an existing Next.js 16 project. Install the Inngest SDK and the local dev CLI. The SDK is what your app imports; the CLI runs a local Inngest server that calls your functions during development.

npm install inngest
npm install --save-dev inngest-cli

Claude Code's training data lags library releases, so before you ask it to write any Inngest code, add a short note to CLAUDE.md so it uses the current v3 patterns and does not invent a v2 API.

## Background Jobs (Inngest)

- Inngest client lives in lib/inngest/client.ts, typed with EventSchemas.
- Every job is inngest.createFunction with an id, a trigger, and a handler.
- Wrap each side effect in step.run("name", async () => {...}). Steps are durable checkpoints.
- Serve route is app/api/inngest/route.ts exporting { GET, POST, PUT }.
- oRPC endpoints send events with inngest.send(...). They never await the job.

That block is enough to keep Claude on the rails. It reads the file at session start and stops reaching for outdated shapes.

The Typed Inngest Client

Everything starts with one client. Defining your event names and payload shapes here (with EventSchemas) makes inngest.send() and your function triggers fully type-safe. Send the wrong event name or a malformed payload and you get a TypeScript error before the code ever runs.

Ask Claude to create the client with a schema for the three events we will send.

// lib/inngest/client.ts
import { EventSchemas, Inngest } from "inngest";

type Events = {
  "user/welcome.email.requested": {
    data: {
      userId: string;
      email: string;
      name: string;
    };
  };
  "media/image.uploaded": {
    data: {
      userId: string;
      imageId: string;
      sourceUrl: string;
    };
  };
  "order/created": {
    data: {
      orderId: string;
      userId: string;
      total: number;
    };
  };
};

export const inngest = new Inngest({
  id: "build-this-now-app",
  schemas: new EventSchemas().fromRecord<Events>(),
});

The id names your app inside Inngest. The Events map is the contract: every event your app sends and every function trigger checks against it.

The Serve Route

Inngest does not run your functions in the same process that sends the event. Instead it calls your app back over HTTP when a job is ready to run. That callback needs an endpoint, and in the App Router that is a route handler.

The serve helper from inngest/next takes your client and the list of functions it should expose, and returns the three HTTP method handlers Inngest uses to register and invoke your jobs. We will fill in the functions array as we build each job.

// app/api/inngest/route.ts
import { serve } from "inngest/next";
import { inngest } from "@/lib/inngest/client";
import { sendWelcomeEmail } from "@/lib/inngest/functions/send-welcome-email";
import { processImage } from "@/lib/inngest/functions/process-image";
import { fanOutOrderWebhooks } from "@/lib/inngest/functions/fan-out-order-webhooks";

export const { GET, POST, PUT } = serve({
  client: inngest,
  functions: [sendWelcomeEmail, processImage, fanOutOrderWebhooks],
});

That is the only route you need. GET lets the Inngest dev server introspect your functions, and POST and PUT handle registration and invocation. You never call this route yourself.

Job One: A Durable Email Send

Here is the simplest useful job. When someone signs up, we send a welcome email through Resend. We do not want the signup request to block on the email, and we do not want a transient Resend hiccup to lose it.

The function triggers on the user/welcome.email.requested event. The event argument is typed from the schema, so event.data.email is known to be a string. The actual send goes inside step.run, which means if Resend returns a 500, Inngest retries just that step (up to 4 times by default) without re-running anything before it.

// lib/inngest/functions/send-welcome-email.ts
import { Resend } from "resend";
import { inngest } from "@/lib/inngest/client";

const resend = new Resend(process.env.RESEND_API_KEY);

export const sendWelcomeEmail = inngest.createFunction(
  { id: "send-welcome-email", retries: 4 },
  { event: "user/welcome.email.requested" },
  async ({ event, step }) => {
    const { email, name } = event.data;

    const result = await step.run("send-via-resend", async () => {
      const { data, error } = await resend.emails.send({
        from: "hello@yourdomain.com",
        to: email,
        subject: `Welcome, ${name}`,
        html: `<p>Hey ${name}, thanks for signing up. Glad you are here.</p>`,
      });

      if (error) {
        throw new Error(`Resend failed: ${error.message}`);
      }

      return data;
    });

    return { sent: true, id: result?.id };
  },
);

Notice what is not here: no try/catch retry loop, no dead-letter queue, no idempotency table. Throwing inside step.run signals a retriable failure. Returning commits the checkpoint. The retries: 4 line is explicit here, but 4 is also the default, so you can drop it.

Job Two: Image Processing With Multiple Steps

The email job has one step. Real jobs have several, and this is where durability earns its keep. When a user uploads an image, we want to download it, resize it, upload the result to storage, and then update the database row. Four side effects, four chances to fail.

Each side effect gets its own step.run. If the storage upload succeeds but the database write fails, Inngest retries from the database write only. It does not re-download or re-resize the image, because those steps already returned and their results are memoized. That saves time, money, and avoids duplicate work against slow services.

We also add a concurrency limit keyed by user, so one person uploading 200 images cannot monopolize the image pipeline for everyone else.

// lib/inngest/functions/process-image.ts
import sharp from "sharp";
import { inngest } from "@/lib/inngest/client";
import { uploadToStorage } from "@/lib/storage";
import { updateImageRecord } from "@/lib/db";

export const processImage = inngest.createFunction(
  {
    id: "process-image",
    retries: 4,
    concurrency: { key: "event.data.userId", limit: 5 },
  },
  { event: "media/image.uploaded" },
  async ({ event, step }) => {
    const { imageId, sourceUrl, userId } = event.data;

    const original = await step.run("download-source", async () => {
      const res = await fetch(sourceUrl);
      if (!res.ok) {
        throw new Error(`Download failed: ${res.status}`);
      }
      const buffer = Buffer.from(await res.arrayBuffer());
      return buffer.toString("base64");
    });

    const resized = await step.run("resize-to-thumbnail", async () => {
      const input = Buffer.from(original, "base64");
      const output = await sharp(input)
        .resize(400, 400, { fit: "cover" })
        .webp({ quality: 80 })
        .toBuffer();
      return output.toString("base64");
    });

    const storedUrl = await step.run("upload-thumbnail", async () => {
      const bytes = Buffer.from(resized, "base64");
      return uploadToStorage(`thumbnails/${imageId}.webp`, bytes, {
        contentType: "image/webp",
      });
    });

    await step.run("update-db-record", async () => {
      await updateImageRecord(imageId, {
        userId,
        thumbnailUrl: storedUrl,
        status: "ready",
      });
    });

    return { imageId, thumbnailUrl: storedUrl };
  },
);

A detail worth calling out: step results are serialized and stored between steps, so we pass the image as a base64 string rather than a raw Buffer. For large files you would upload the original to storage and pass the URL between steps instead of the bytes, keeping each checkpoint small. Claude does the naive buffer-passing version unless you tell it about this, so it is worth a line in your prompt for anything over a megabyte.

Job Three: Webhook Fan-Out

The last pattern is fan-out: one event needs to notify several downstream systems. When an order is created, maybe you ping your analytics service, your fulfillment partner, and a Slack channel. If Slack is down, you do not want to skip analytics.

The clean way to do this is one step.run per destination. Each is independently retried. A failure to reach Slack retries the Slack step alone while the analytics and fulfillment steps stay committed. Running them as separate steps (rather than one big step that loops) is what gives you per-destination retries.

// lib/inngest/functions/fan-out-order-webhooks.ts
import { inngest } from "@/lib/inngest/client";

async function postJson(url: string, body: unknown) {
  const res = await fetch(url, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    throw new Error(`POST ${url} failed: ${res.status}`);
  }
  return res.status;
}

export const fanOutOrderWebhooks = inngest.createFunction(
  { id: "fan-out-order-webhooks", retries: 4 },
  { event: "order/created" },
  async ({ event, step }) => {
    const { orderId, userId, total } = event.data;
    const payload = { orderId, userId, total };

    await step.run("notify-analytics", async () => {
      return postJson(process.env.ANALYTICS_WEBHOOK_URL!, payload);
    });

    await step.run("notify-fulfillment", async () => {
      return postJson(process.env.FULFILLMENT_WEBHOOK_URL!, payload);
    });

    await step.run("notify-slack", async () => {
      return postJson(process.env.SLACK_WEBHOOK_URL!, {
        text: `New order ${orderId} for $${(total / 100).toFixed(2)}`,
      });
    });

    return { orderId, notified: 3 };
  },
);

If you have destinations that are safe to hit in parallel, you can wrap each step.run in a Promise.all. Inngest runs them concurrently and still checkpoints each one. For most fan-outs the sequential version above is fine and easier to reason about.

Triggering Jobs From oRPC

Now the part that ties it together. Your oRPC endpoints do the fast, synchronous work (validate input, write the primary record) and then hand the slow work to Inngest with a single inngest.send(...). Sending an event is a quick HTTP call to Inngest that returns immediately. The endpoint does not wait for the job to run.

Here is a signup procedure that creates the user, sends the welcome-email event, and returns. Zod validates the input, and because the event name and payload are typed by the client schema, a typo in either is a compile error.

// lib/orpc/procedures/auth.ts
import { z } from "zod";
import { os } from "@orpc/server";
import { inngest } from "@/lib/inngest/client";
import { createUser } from "@/lib/db";

export const signUp = os
  .input(
    z.object({
      email: z.string().email(),
      name: z.string().min(1),
      password: z.string().min(8),
    }),
  )
  .handler(async ({ input }) => {
    const user = await createUser({
      email: input.email,
      name: input.name,
      password: input.password,
    });

    await inngest.send({
      name: "user/welcome.email.requested",
      data: {
        userId: user.id,
        email: user.email,
        name: user.name,
      },
    });

    return { id: user.id, email: user.email };
  });

The createUser call is awaited because the response depends on it. The inngest.send call is awaited too, but only to confirm Inngest accepted the event, which takes a few milliseconds. The actual email send happens later, in the background, retried if needed. The user gets their response the instant the account exists.

The order endpoint follows the same shape: write the order, fire the order/created event, return. The three webhooks fan out on their own, and the endpoint never blocks on them.

One rule keeps this clean: endpoints send events, functions do work. If you feel tempted to await the job's result inside the endpoint, that is a sign the work belongs on the request path after all, or that you need a status field the client can poll.

Running It Locally

The Inngest dev server discovers your functions through the serve route, then calls them when events fire, all on your machine with no cloud account required. Start your Next.js dev server, then start the Inngest CLI pointed at your serve route.

# terminal one
npm run dev

# terminal two
npx inngest-cli dev -u http://localhost:3000/api/inngest

Open the dev dashboard at http://localhost:8288. You get a live view of every event, every function run, each step's input and output, and a replay button. Trigger a signup through your app and watch the welcome-email function execute step by step. If you throw an error inside a step on purpose, you can watch the retry happen in real time.

This local loop is the fastest way to confirm a job behaves before it ships. You see exactly which step failed and why, without adding a single log line.

Let Claude Run the Quality Gates

Background jobs are easy to get subtly wrong: an un-awaited send, a step that mutates outside its own boundary, an event payload that drifts from the schema. Ask Claude to check the whole wiring before you commit.

claude "run tsc --noEmit and fix any type errors, then confirm every inngest.send call
matches an event name in lib/inngest/client.ts and every createFunction trigger exists
in the schema"

Because the events are typed at the client, most drift shows up as a type error at the send call site. Claude reads the compiler output, traces the mismatch back to the schema, and fixes whichever side is wrong. Type check clean, lint clean, build clean is the bar before this ships.

Where the $29 Code Kit Fits

You can build all of this by hand with Claude Code and a paid Anthropic plan (Claude Code needs one to run). What the $29 Code Kit adds, for a one-time $29 with no subscription, is a harness around that same loop: it plans the job before writing it, builds it, evaluates the output against your schema and conventions, tests it against the running Inngest dev server, and holds every change to the same quality gates used above (zero type errors, zero lint errors, a clean Turbopack build) before it is considered done. It is not a different way to write Inngest functions. It is the plan, build, evaluate, test discipline wired up so you do not run each step by hand every time.


Push slow work off the request path, wrap each side effect in a step, cap concurrency where a downstream can be overwhelmed, and let the typed client catch payload drift at compile time. That is a durable job system in a few files, and your endpoints stay fast because they only ever send an event and return.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

Posted by @speedy_devv

Continue in Workflow

  • Commerce agentique : comment construire une app que les agents IA peuvent payer
    Un guide en français simple du commerce agentique en 2026 : ce que font x402, ACP et le Machine Payments Protocol, plus un pas-à-pas d'un week-end pour livrer une API payante que les agents IA peuvent acheter.
  • Bonnes pratiques Claude Code
    Cinq habitudes séparent les ingénieurs qui livrent avec Claude Code : les PRDs, les règles CLAUDE.md modulaires, les slash commands personnalisés, les resets /clear, et un état d'esprit d'évolution du système.
  • Le mode auto de Claude Code
    Un second modèle Sonnet examine chaque appel d'outil Claude Code avant qu'il s'exécute. Ce que le mode auto bloque, ce qu'il autorise, et les règles d'autorisation qu'il place dans tes paramètres.
  • Channels, Routines, Teleport, Dispatch
    Les quatre fonctionnalités Claude Code livrées par Anthropic en mars et avril 2026 qui transforment le CLI en une couche de coordination orientée événements, entre téléphone, web et desktop.
  • Claude Code 1M Context in Practice: When Bigger Isn't Better
    The 1M-token context window is GA at flat pricing, but bigger isn't always better. A decision framework, token-cost math, and when to use /compact, subagents, and dynamic workflows instead.
  • How to Build an Admin Dashboard With Claude Code
    Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

More from Handbook

  • Principes de base de l'agent
    Cinq façons de construire des agents spécialisés dans le code Claude : Sous-agents de tâches, .claude/agents YAML, commandes slash personnalisées, personas CLAUDE.md, et invites de perspective.
  • L'ingénierie du harness agent
    Le harness, c'est toutes les couches autour de ton agent IA sauf le modèle lui-même. Découvre les cinq leviers de contrôle, le paradoxe des contraintes, et pourquoi le design du harness détermine les performances de l'agent bien plus que le modèle.
  • Patterns d'agents
    Orchestrateur, fan-out, chaîne de validation, routage par spécialiste, raffinement progressif, et watchdog. Six formes d'orchestration pour câbler des sub-agents Claude Code.
  • Meilleures pratiques des équipes d'agents
    Patterns éprouvés pour les équipes d'agents Claude Code. Prompts de création riches en contexte, tâches bien calibrées, propriété des fichiers, mode délégué, et correctifs v2.1.33-v2.1.45.

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →

How to Add File Uploads With Claude Code (Supabase Storage)

Build secure user file and image uploads end to end with Claude Code: private Supabase Storage buckets, per-user RLS, signed URLs, and an upload UI wired through a type-safe oRPC API.

How to Build an Admin Dashboard With Claude Code

Ship an internal admin panel with Claude Code: role-gated routes, a searchable users and orders table with pagination, impersonation-safe RLS, and metrics tiles pulled through a type-safe API.

On this page

What "Durable" Actually Buys You
Install and Point Claude at the Docs
The Typed Inngest Client
The Serve Route
Job One: A Durable Email Send
Job Two: Image Processing With Multiple Steps
Job Three: Webhook Fan-Out
Triggering Jobs From oRPC
Running It Locally
Let Claude Run the Quality Gates
Where the $29 Code Kit Fits

Arrête de tout configurer. Place à la construction.

Des templates SaaS avec orchestration IA.

Découvrez ce que nous construisons pour les entreprises →