Inngest

Docs › Introduction

What is Inngest?

Durable functions for agents and workflows, with isolated code execution, live progress, and outcome scoring built in.

Inngest makes your code durable. You write functions in your existing app and break the work into steps. Inngest runs each step, saves its result, and retries failed work from the step that failed. Every run has a trace that shows what ran, what waited, and what failed.

The rest of the platform builds on the same run. Sandboxes run code in isolation inside it, Realtime streams its progress to users, and Agent Evals connect later feedback to it. Together, they let you see what ran, recover from failures, run untrusted code safely, and measure whether each change actually helped.

An example run

A support agent answers a customer ticket. The same pattern works for background jobs, data pipelines, and other workflows.

Diagram: A support agent answers a customer ticket. The same pattern works for background

Here's that function. Each step.run() is saved, so a failed reply retries without reloading context or redrafting the answer. group.experiment() picks the answer strategy, step.realtime.publish() updates the customer, and step.sandbox runs diagnostics in an isolated VM. When the customer rates the answer days later, a deferred scorer credits that rating to the chosen variant (in TS).

TypeScript
const ticketChannel = realtime.channel({
  name: ({ ticketId }: { ticketId: string }) => `ticket:${ticketId}`,
  topics: { status: { schema: z.object({ message: z.string() }) } },
});

export const answerTicket = inngest.createFunction(
  { id: "answer-ticket", triggers: { event: "support/ticket.created" } },
  async ({ event, step, group }) => {
    const { ticketId } = event.data;
    const channel = ticketChannel({ ticketId });

    // Each step's result is saved. A retry skips steps that already finished.
    const context = await step.run("load-context", () =>
      loadTicketContext(ticketId)
    );

    // Pick variant A or B, then draft the answer with that strategy.
    const { result: answer } = await group.experiment("answer-style", {
      variants: {
        concise: () =>
          step.run("draft-concise", () => draftAnswer(context, "concise")),
        detailed: () =>
          step.run("draft-detailed", () => draftAnswer(context, "detailed")),
      },
      select: experiment.weighted({ concise: 50, detailed: 50 }),
    });

    // Stream progress to the customer's browser.
    await step.realtime.publish("drafted", channel.status, {
      message: "Checking your account…",
    });

    // Run diagnostics in an isolated Sandbox.
    const sandbox = await step.sandbox.create("create-sandbox", {
      name: `diagnose-${ticketId}`,
      vcpu: 1,
      memoryMb: 1024,
    });
    const diagnostics = await sandbox.commands.run(
      "run-diagnostics",
      "./diagnose.sh",
      { timeout: "30s" }
    );
    await sandbox.destroy("destroy-sandbox");

    await step.run("send-reply", () =>
      sendReply(ticketId, `${answer}\n\n${diagnostics.stdout}`)
    );
  }
);
Python
# Requires the inngest release after 0.5.19 (for group.experiment).
import inngest
from inngest.experimental import experiment, realtime

from .stubs import draft_answer, load_ticket_context, send_reply

inngest_client = inngest.Inngest(app_id="support-app")

@inngest_client.create_function(
    fn_id="answer-ticket",
    trigger=inngest.TriggerEvent(event="support/ticket.created"),
)
async def answer_ticket(ctx: inngest.Context) -> None:
    ticket_id = str(ctx.event.data["ticketId"])

    # Each step's result is saved. A retry skips steps that already finished.
    async def load_context() -> dict[str, list[str]]:
        return await load_ticket_context(ticket_id)

    context = await ctx.step.run("load-context", load_context)

    async def draft_concise() -> str:
        return await draft_answer(context, "concise")

    async def draft_detailed() -> str:
        return await draft_answer(context, "detailed")

    # Pick variant A or B, then draft the answer with that strategy.
    selected = await ctx.group.experiment(
        "answer-style",
        variants={
            "concise": lambda: ctx.step.run("draft-concise", draft_concise),
            "detailed": lambda: ctx.step.run("draft-detailed", draft_detailed),
        },
        select=experiment.bucket(
            ctx.run_id, weights={"concise": 50, "detailed": 50}
        ),
    )
    answer = str(selected.result)

    # Stream progress to the customer's browser.
    async def publish_status() -> None:
        await realtime.publish(
            client=inngest_client,
            channel=f"ticket:{ticket_id}",
            topic="status",
            data={"message": "Checking your account…"},
        )

    await ctx.step.run("drafted", publish_status)

    # Sandboxes aren't available in Python yet, so this version skips the
    # diagnostics step.
    async def reply() -> None:
        await send_reply(ticket_id, answer)

    await ctx.step.run("send-reply", reply)
Go
import (
	"context"
	"encoding/json"

	"github.com/inngest/inngestgo"
	"github.com/inngest/inngestgo/realtime"
	"github.com/inngest/inngestgo/step"
)

type TicketCreated struct {
	TicketID string `json:"ticketId"`
}

func AnswerTicket(client inngestgo.Client) (inngestgo.ServableFunction, error) {
	return inngestgo.CreateFunction(
		client,
		inngestgo.FunctionOpts{ID: "answer-ticket"},
		inngestgo.EventTrigger("support/ticket.created", nil),
		func(ctx context.Context, input inngestgo.Input[TicketCreated]) (any, error) {
			ticketID := input.Event.Data.TicketID

			// Each step's result is saved. A retry skips steps that already finished.
			tc, err := step.Run(ctx, "load-context", func(ctx context.Context) (TicketContext, error) {
				return loadTicketContext(ctx, ticketID)
			})
			if err != nil {
				return nil, err
			}

			answer, err := step.Run(ctx, "draft-answer", func(ctx context.Context) (string, error) {
				return draftAnswer(ctx, tc)
			})
			if err != nil {
				return nil, err
			}

			// Stream progress to the customer's browser.
			_, err = step.Run(ctx, "drafted", func(ctx context.Context) (any, error) {
				data, err := json.Marshal(map[string]string{"message": "Checking your account…"})
				if err != nil {
					return nil, err
				}
				return nil, realtime.Publish(ctx, "ticket:"+ticketID, "status", data)
			})
			if err != nil {
				return nil, err
			}

			// Sandboxes and experiments aren't available in Go yet, so this
			// version drafts one answer and skips the diagnostics step.
			_, err = step.Run(ctx, "send-reply", func(ctx context.Context) (any, error) {
				return nil, sendReply(ctx, ticketID, answer)
			})
			return nil, err
		},
	)
}

How Inngest works

  1. Durable Execution

    An event, schedule, or API request starts a run. Inngest records completed steps, resumes after waits, retries failed work from where the function left off, and controls how much work runs at once. In the example, if the reply fails to send, only that step runs again.

    Use durable functions as background workflows or as durable endpoints that return an HTTP response.

  2. Sandboxes

    When a step needs its own execution environment, such as to run generated code or test a repository, run that work in a Sandbox. Each Sandbox is a microVM with its own filesystem and processes, isolated from your app and other Sandboxes. You can pause and resume a Sandbox, or snapshot it and clone a new one. Sandbox activity appears in the same run trace as the surrounding steps. In the example, the agent runs a diagnostic script in a Sandbox before it replies.

    Sandboxes run code inside a run. They aren't a way to host your Inngest functions. Read Sandboxes for lifecycle, isolation, and cleanup details.

  3. Agent Evals

    A run can succeed and still fail to help the customer. Agent Evals connect outcome scores to the runs that produced them, so you can judge changes by their results:

    • Sessions group related runs, such as every run for one ticket.
    • Scores record a named number or true/false value for a run or step. Score right away, or defer the score until feedback arrives. A deferred score is credited to the original run.
    • Experiments use group.experiment() to select a variant for each run. A variant can be one step or a whole workflow path. Pass the experiment reference with a score so a later outcome counts toward the variant that produced it.

    In the example, the customer's rating arrives days later. It's scored against the original run and its variant, so you can compare answer strategies, then open traces to see how each result was produced.

  4. Realtime

    Realtime sends typed messages from a function to authorized clients, so users can follow work as it runs. Use step.realtime.publish() for state changes and final results so a retry doesn't send the message twice. Use inngest.realtime.publish() for frequent updates such as token streams. In the example, the customer sees the agent's progress while it drafts an answer.

Where your code runs

Your functions run on your own infrastructure, in any cloud or on your servers. Inngest calls them through an HTTP serve() endpoint, or they connect to Inngest as a Connect worker. Inngest coordinates durable execution while your code runs where it already lives. Use Inngest Cloud, or self-host Inngest.

Sandboxes are different: they run on securely isolated VMs that Inngest manages, so you don't host or scale that infrastructure yourself. Your function starts and controls them as steps in its run.

Explore the platform

Durable Execution

Coordinate steps, retries, waits, and events so work finishes after failures.

inngest-kb.zunoywork.com
Sandboxes

Run code in isolated microVMs as part of a durable run.

inngest.com
Agent Evals

Score outcomes and compare changes to steps and workflows.

inngest.com
Realtime

Stream progress from durable runs to your users.

inngest.com
Flow control

Control concurrency, throughput, and fairness across tenants.

inngest-kb.zunoywork.com
Observability

Inspect traces, replay runs, and track metrics in production.

inngest.com

Start building

Pick your language to build your first durable function. Use Local Development to run and inspect it on your machine with the Dev Server.

TypeScript

Works with Node.js, Bun, Deno, and edge runtimes.

inngest.com
Python

Sync and async functions with FastAPI, Flask, or Django.

inngest.com
Go

Typed events and durable steps with the standard library.

inngest.com
Next.js
inngest-kb.zunoywork.com
Node.js
inngest.com
Express
inngest-kb.zunoywork.com
TanStack Start
inngest-kb.zunoywork.com
NestJS
inngest-kb.zunoywork.com
Astro
inngest-kb.zunoywork.com
H3
inngest-kb.zunoywork.com
FastAPI & Flask
inngest.com

Next steps

Updated Nov 10, 2026

Powered byZunoy