Inngest brings durable execution to JavaScript. Write a function, decorate it, and Inngest handles retries, delays, fan-out, and step-level checkpointing — all without managing queues, workers, or databases yourself. For AI applications, this means you can build multi-step agents that survive failures, rate-limit themselves against OpenAI, and resume mid-chain without losing work.

This guide walks through adding Inngest to a Next.js app, writing a durable AI agent function, and running it locally.

How Inngest Works

Inngest works via a single HTTP endpoint in your app. When an event is sent, Inngest calls your endpoint with the event payload. Your function runs step-by-step; each `step.run()` call is checkpointed. If the function crashes mid-way, Inngest re-invokes it and fast-forwards past completed steps (they return cached results instead of re-executing).

Concept Description
Event A named JSON payload that triggers functions
Function An Inngest function — runs durably via `step.run()`
Step A named, retriable unit of work inside a function
Flow control `step.sleep()`, `step.waitForEvent()`, `step.invoke()`

Installation

npm install inngest # For Next.js specifically: npm install @inngest/next

Create the Inngest Client

// lib/inngest.ts import { Inngest } from "inngest"; export const inngest = new Inngest({ id: "my-ai-app", // eventKey is only needed when sending events from outside your server });

Create the Serve Route

This is the HTTP endpoint Inngest calls to execute your functions:

// app/api/inngest/route.ts import { serve } from "inngest/next"; import { inngest } from "@/lib/inngest"; import { researchAgent } from "@/lib/functions/research-agent"; export const { GET, POST, PUT } = serve({ client: inngest, functions: [researchAgent], });

Write a Durable AI Agent Function

// lib/functions/research-agent.ts import { inngest } from "@/lib/inngest"; import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); export const researchAgent = inngest.createFunction( { id: "research-agent", name: "AI Research Agent", retries: 3, throttle: { // Rate-limit to 10 runs per minute (respects OpenAI rate limits) count: 10, period: "1m", }, }, { event: "research/requested" }, async ({ event, step }) => { const { query, userId } = event.data; // Step 1: Expand the query into sub-questions const subQuestions = await step.run("expand-query", async () => { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "Break this research query into 3 specific sub-questions. Return JSON array.", }, { role: "user", content: query }, ], response_format: { type: "json_object" }, }); const parsed = JSON.parse(response.choices[0].message.content!); return parsed.questions as string[]; }); // Step 2: Research each sub-question in parallel const answers = await step.run("research-questions", async () => { // Note: fan-out with step.invoke() for true parallel durable execution const results = await Promise.all( subQuestions.map(async (q) => { const response = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "Answer this question concisely with evidence." }, { role: "user", content: q }, ], }); return { question: q, answer: response.choices[0].message.content }; }) ); return results; }); // Step 3: Synthesize a final report const report = await step.run("synthesize-report", async () => { const context = answers .map((a) => `Q: ${a.question} A: ${a.answer}`) .join(" "); const response = await openai.chat.completions.create({ model: "gpt-4o", messages: [ { role: "system", content: "Synthesize these research findings into a coherent summary." }, { role: "user", content: `Original query: ${query} Findings: ${context}` }, ], }); return response.choices[0].message.content!; }); // Step 4: Save to database await step.run("save-report", async () => { await saveReport({ userId, query, report }); }); return { report, subQuestionsCount: subQuestions.length }; } );

Trigger the Function

// app/api/research/route.ts import { inngest } from "@/lib/inngest"; import { NextResponse } from "next/server"; export async function POST(req: Request) { const { query, userId } = await req.json(); await inngest.send({ name: "research/requested", data: { query, userId }, }); return NextResponse.json({ status: "queued" }); }

Local Development

# Terminal 1: Run Next.js npm run dev # Terminal 2: Run the Inngest Dev Server npx inngest-cli@latest dev # The dev server starts at http://localhost:8288 # It auto-discovers your serve route and shows all function runs

The Inngest Dev Server is a full local dashboard — you can trigger events manually, inspect step outputs, replay runs, and view retry history. No account needed for local development.

Environment Variables

# .env.local INNGEST_EVENT_KEY=your-event-key # from app.inngest.com INNGEST_SIGNING_KEY=your-signing-key # from app.inngest.com # Not needed locally — Inngest Dev Server handles auth

Deploying to Production

Deploy your Next.js app normally (Vercel, Railway, Render, etc.). Then in the Inngest dashboard, add your app's serve URL (e.g., `https://yourapp.com/api/inngest`) as an App. Inngest will auto-discover your functions and start routing events to them.