Primitives and steps

Use durable primitives to save progress, wait without running code, and coordinate independent work.

Put an external call in step.run when its result must survive a retry. Use a wait when the next action depends on time or another event. Give each step a stable ID so an active run can find its saved progress after a deployment.

Why use steps

Inngest saves the result of each completed step. If a later step fails, Inngest can retry that work and reuse the earlier result. A sleep or event wait pauses the run without keeping your function process active. The run trace shows which step completed, waited, or retried.

Benefits of using steps

  • Improved reliability: Structured steps give you precise control over each task in a function.
  • Error handling: Capturing and managing errors at the step level means better error recovery.
  • Retries: A failing step can be retried and recovered on its own, without re-running steps that already succeeded.
  • Independent testing: You can test and debug each step separately from the others.
  • Readable code: Small, named steps make code easier to navigate and refactor.

To learn more about how steps run, see Anatomy of workflows: runs, steps, and checkpoints.

Anatomy of a step

Each step.run() call names the work and supplies a callback:

const order = await step.run("load-order", () =>
  getOrder(event.data.orderId)
);

await step.run("send-receipt", () => sendReceipt(order));

"load-order" is the step ID. Inngest runs the callback and saves its returned order. If send-receipt fails, Inngest retries it. When the handler runs again, the SDK returns the saved order without calling getOrder again. Code outside steps can run again.

Keep step IDs stable

A function ID identifies the function; a step ID identifies work within its run. Keep each logical step's ID stable across deployments so an active run can find its saved result. If you need an active run to execute changed step logic, changing the ID makes it new work. See Versioning before changing IDs in a deployed function.

A step does not make an external action exactly once. If a provider accepts a request but its response is lost, the step can retry the action. Use an idempotency key for writes, payments, and messages that must not happen twice. See Idempotency.

Step primitives

  • step.run — Run code, such as an API call or database write, as a retriable step. Use it when the workflow must keep the result or retry the work on failure.
  • step.sleep — Resume after a duration, such as 30 minutes.
  • step.sleepUntil — Resume at a specific date and time.
  • step.waitForEvent — Resume when a matching event arrives, or handle the timeout. Use this for most external actions and events that can affect multiple runs.
  • step.waitForSignal — Resume one run with a unique signal. Use it only when the run needs direct, latency-sensitive resumption.
  • step.invoke — Start another Inngest function and wait for its result. Use it to reuse or compose functions.
  • step.sendEvent — Send an event from inside a function as a reliable step. Use it to trigger other functions without losing the send when this function retries.

Other primitives

  • defer — Start one typed function in its own run without waiting for its result.
  • Parallel steps — Run independent steps concurrently and collect their results.
  • group.experiment — Select a durable variant for a run so you can compare outcomes.
  • Metadata — Attach custom data to a run or step so you can find and analyze it later.

Workflow and endpoint support

Every primitive works in durable workflows. Most also work in durable endpoints:

PrimitiveDurable workflowsDurable endpoints
step.run✓✓
step.sleep✓✓
step.sleepUntil✓✓
step.waitForEvent✓✓
step.waitForSignal✓✓
step.invoke✓✓
step.sendEvent✓✓
Parallel steps✓✓
defer✓—
group.experiment✓—

In a durable endpoint, a sleep or wait turns the request async and redirects the caller to wait for the result.

Keep runs predictable

Put side effects in steps so a resumed run can reuse completed results. Handle timeouts for waits and invocations. Step results must be JSON-serializable; return only the data later steps need.

Limits

  • Steps per function: 1,000. Process items within a step, or fan out to more runs.
  • Step result size: 4 MiB per step. Store large results elsewhere and return a reference.
  • Run state: 32 MiB across event data, step results, and the function's return value.
  • Step duration: up to 2 hours, or less if your host has a shorter timeout.
  • Sleeps: up to 1 year with step.sleep() or step.sleepUntil().

See Limits for every platform limit.