Skip to content

Middleware lifecycle

Middleware lets you hook into function execution to add cross-cutting concerns like logging, error handling, and dependency injection. Middleware is class-based: you extend Middleware.BaseMiddleware and define hook methods.

Creating middleware

Extend Middleware.BaseMiddleware and override the hooks you need:

ts
import { Middleware } from "inngest";

class MyMiddleware extends Middleware.BaseMiddleware {
  id = "my-middleware";
  // Override hook methods here
}

Register middleware at the client level (applies to all functions) or the function level (applies to one function):

ts
// Client level
const inngest = new Inngest({
  id: "my-app",
  middleware: [MyMiddleware],
});

// Function level
inngest.createFunction({
  id: "my-fn",
  middleware: [MyMiddleware],
  triggers: { event: "app/user.created" },
}, async ({ event }) => {
  // ...
});

Callout: A fresh middleware instance is created for every request, so you can safely use instance properties (this) to store per-request state without worrying about leaks between runs.

Execution lifecycle

The following shows the order in which hooks are called during a request. This is the key mental model for understanding middleware:

  1. wrapRequest() - Outermost wrapper around the entire HTTP request
  2. transformFunctionInput() - Modify ctx before the function handler runs
  3. wrapFunctionHandler() - Wrap function execution (e.g. for AsyncLocalStorage)
  4. onMemoizationEnd() - After all memoized steps resolve
  5. onRunStart() - First attempt only (attempt 0, no memoized steps)
  6. Per step:
    • transformStepInput()wrapStep()onStepStart()wrapStepHandler() → execute → onStepComplete() / onStepError()
  7. onRunComplete() / onRunError() - When the function finishes

Event sending (transformSendEvent()wrapSendEvent()) is not part of this fixed sequence. It runs whenever inngest.send() or step.sendEvent() is called.

Callout: Hooks you don't define have zero overhead: the SDK skips them entirely. Only override the hooks you need.

Observable hooks

Observable hooks (on*) are call-and-forget. They receive read-only arguments and do not return a value. Use them for logging, metrics, and side effects. Errors thrown in observable hooks are caught and logged, not propagated to the run or step.


onRunStart

Called once on the very first attempt of a run (attempt 0, no memoized steps). Not called on subsequent retries or replays.

Will not call if the first attempt fails to reach the app (e.g. a network error).

  • ctx: The function context.
  • functionInfo: Metadata about the function being executed.

onRunComplete

Called when a function completes successfully.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • output: The successful return value of the function.

onRunError

Called each time a function throws an error.

  • ctx: The function context.
  • error: The error that was thrown.
  • functionInfo: Metadata about the function.
  • isFinalAttempt: Whether this is the last retry attempt before the run permanently fails.

onStepStart

Called before a step handler runs. Only called for step.run and step.sendEvent.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • stepInfo: Metadata about the step being executed.

onStepComplete

Called when a step succeeds. Only called for step.run and step.sendEvent. Never called for memoized steps.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • output: The successful return value of the step.
  • stepInfo: Metadata about the step.

onStepError

Called when a step throws an error. Only called for step.run and step.sendEvent. Never called for memoized steps.

  • ctx: The function context.
  • error: The error that was thrown.
  • functionInfo: Metadata about the function.
  • isFinalAttempt: Whether this is the last retry attempt for this step.
  • stepInfo: Metadata about the step.

onMemoizationEnd

Called once per request after all memoized steps have resolved. On the first request (no memoized steps), called immediately.

Use cases for this hook are limited. It's primarily useful for logging or metrics that should run after all memoized steps have resolved.

  • ctx: The function context.
  • functionInfo: Metadata about the function.

Wrapping hooks

Wrapping hooks (wrap*) follow an onion model: you must call next() to continue processing. Code before next() runs on the way in, code after runs on the way out.

ts
class MyMiddleware extends Middleware.BaseMiddleware {
  id = "my-middleware";
  async wrapFunctionHandler({ next }: Middleware.WrapFunctionHandlerArgs) {
    console.log("before");
    const result = await next();
    console.log("after");
    return result;
  }
}

With multiple middleware, they nest: middleware 1 wraps middleware 2 wraps the inner handler.


wrapRequest

Wraps the entire HTTP request

Example use cases: auth, top-level metrics, or error boundaries.

  • functionInfo: Metadata about the function.
  • next: Must call to continue processing. Returns the HTTP response.
  • requestInfo: The incoming HTTP request metadata (headers, URL, method).
  • runId: The ID of the current run.

wrapFunctionHandler

Wraps function execution. next() resolves when the function completes.

Example use cases: AsyncLocalStorage, error transformation, timing, or inserting steps.

Callout: next() only resolves when the function fully completes or errors. When a new step is discovered, next() never resolves for that request. It intentionally hangs until garbage collection deletes it.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • next: Must call to execute the function handler.

wrapStep

Wraps every step, including memoized steps and all step kinds (step.run, step.sleep, step.invoke, etc.).

Example use cases: deserialize memoized data, insert steps.

Callout: If the step is not memoized, next() never resolves for that request. The method intentionally hangs until garbage collection deletes it.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • next: Must call to continue processing the step.
  • stepInfo: Metadata about the step. Check stepInfo.memoized to differentiate memoized vs fresh.

wrapStepHandler

Wraps step handler execution. Only called for step.run and step.sendEvent.

Example use cases: serialize step output, error handling, or timing.

  • ctx: The function context.
  • functionInfo: Metadata about the function.
  • next: Must call to execute the step handler.
  • stepInfo: Metadata about the step.

wrapSendEvent

Wraps event sending via inngest.send() or step.sendEvent().

Example use cases: backup on send failure, metrics.

  • events: The events being sent.
  • functionInfo: Metadata about the function, or null if called outside a function (e.g. inngest.send()).
  • next: Must call to send the events.

Transform hooks

Transform hooks (transform*) receive arguments and return a modified copy. Use them to inject dependencies, modify inputs, or enrich events.


transformFunctionInput

Modify the function context before the handler runs.

Example use cases: dependency injection, deserialize event data.

  • ctx: The function context. Add properties here to inject them into the function handler.
  • functionInfo: Metadata about the function.
  • steps: A record of memoized step data keyed by hashed step ID.

transformStepInput

Modify step options or input before a step runs.

Example use cases: serialize step.invoke data, bust memoization cache (i.e. change step ID).

  • functionInfo: Metadata about the function.
  • stepInfo: Partial step metadata.
  • stepOptions: The options passed to the step (first argument).
  • input: Arguments passed to the step function (after ID and handler).

transformSendEvent

Modify events before they are sent.

Example use cases: serialize event data, add metadata.

  • events: The events being sent. Return a modified copy to change them.
  • functionInfo: Metadata about the function, or null if called outside a function.

Output type transforms

Output type transforms are declare properties that control how TypeScript types function and step return values. They have no runtime behavior — they only affect the type system.

By default, the SDK assumes all return values are serialized to JSON, so types like Date become string. If your middleware changes serialization behavior at runtime (e.g. via wrapStepHandler), declare a corresponding type transform so TypeScript reflects the actual runtime types.

Callout: Type transforms only affect types. You are responsible for ensuring the declared transform matches your runtime transformation.

Both transforms use the Middleware.StaticTransform pattern to imitate higher-kinded types. For example, a normal generic type that preserves Date instead of Jsonifying it:

ts
// A normal generic type
type PreserveDate<In> = In extends Date ? Date : Jsonify<In>;

As a StaticTransform, this becomes an interface where this["In"] replaces the generic parameter and Out is the result:

ts
import { Middleware } from "inngest";

// The same logic as a StaticTransform
interface PreserveDate extends Middleware.StaticTransform {
  Out: this["In"] extends Date ? Date : Jsonify<this["In"]>;
}
  • In: The original return type. Set automatically by the SDK — do not set this yourself.
  • Out: The transformed type. Define this to compute the output type based on In.

When multiple middleware declare transforms, they are chained in registration order: the Out of one becomes the In of the next.


functionOutputTransform

Declares how function return types are transformed. By default, return types are Jsonified (e.g. Date becomes string).

ts
import { Middleware } from "inngest";

interface PreserveDate extends Middleware.StaticTransform {
  Out: this["In"] extends Date ? Date : Jsonify<this["In"]>;
}

class MyMiddleware extends Middleware.BaseMiddleware {
  id = "my-middleware";
  declare functionOutputTransform: PreserveDate;
}

stepOutputTransform

Declares how step output types are transformed. By default, output types are Jsonified (e.g. Date becomes string).

ts
import { Middleware } from "inngest";

interface PreserveDate extends Middleware.StaticTransform {
  Out: this["In"] extends Date ? Date : Jsonify<this["In"]>;
}

class MyMiddleware extends Middleware.BaseMiddleware {
  id = "my-middleware";
  declare stepOutputTransform: PreserveDate;
}

Static hooks

onRegister

Called once when the middleware class is registered with a client or function.

Example use cases: one-time setup.

ts
class MyMiddleware extends Middleware.BaseMiddleware {
  id = "my-middleware";
  static onRegister({ client, functionInfo }: Middleware.OnRegisterArgs) {
    // One-time setup
  }
}
  • client: The Inngest client instance.
  • functionInfo: Metadata about the function, or null for client-level middleware.

Important notes

  • Performance - Undefined hooks have zero overhead. The SDK checks for hook presence and skips entirely if not defined.
  • Immutability - Observable and wrapping hook arguments are deeply read-only. Do not mutate them.
  • next() is required - Wrapping hooks must call next() or the request will hang.
  • Instance per request - A new middleware instance is created for each request, so instance state is safe to use within a single request.