Introduction
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.

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).
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}`)
);
}
);# 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)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
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.
- Events and triggers start work from events, schedules, webhooks, or another function.
- Steps save progress so retries skip completed work.
- Retries recover failed work automatically.
- Wait for events or sleep until later without keeping compute running.
- Flow control limits concurrency and throughput to protect downstream services.
- Idempotency prevents duplicate events from repeating work.
Use durable functions as background workflows or as durable endpoints that return an HTTP response.
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.
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.
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. Useinngest.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
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.
Next steps
- Concepts defines runs, triggers, steps, waits, and retries.
- Glossary explains the terms used across the docs.
- Deploying functions compares HTTP serving and Connect.
- Platform and operations covers traces, replay, environments, and keys.
Updated Nov 10, 2026
Powered byZunoy