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:

import { Middleware } from "inngest";

class MyMiddleware extends Middleware.BaseMiddleware {
  // Override hook methods here
}

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

// 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 }) => {
  // ...
});

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.

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

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function being executed.


onRunComplete

Called when a function completes successfully.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    output
    Type
    Required
    required
    Description

    The successful return value of the function.


onRunError

Called each time a function throws an error.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    error
    Type
    Required
    required
    Description

    The error that was thrown.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    isFinalAttempt
    Type
    Required
    required
    Description

    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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    output
    Type
    Required
    required
    Description

    The successful return value of the step.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    error
    Type
    Required
    required
    Description

    The error that was thrown.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    isFinalAttempt
    Type
    Required
    required
    Description

    Whether this is the last retry attempt for this step.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    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.

class MyMiddleware extends Middleware.BaseMiddleware {
  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.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    requestInfo
    Type
    Required
    required
    Description

    The incoming HTTP request metadata (headers, URL, method).

  • Name
    runId
    Type
    Required
    required
    Description

    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.

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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.


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.

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

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    Metadata about the step.


wrapSendEvent

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

Example use cases: backup on send failure, metrics.

  • Name
    events
    Type
    Required
    required
    Description

    The events being sent.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function, or null if called outside a function (e.g. inngest.send()).

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.

  • Name
    ctx
    Type
    Required
    required
    Description

    The function context. Add properties here to inject them into the function handler.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    steps
    Type
    Required
    required
    Description

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

  • Name
    functionInfo
    Type
    Required
    required
    Description

    Metadata about the function.

  • Name
    stepInfo
    Type
    Required
    required
    Description

    Partial step metadata.

  • Name
    stepOptions
    Type
    Required
    required
    Description

    The options passed to the step (first argument).

  • Name
    input
    Type
    Required
    required
    Description

    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.

  • Name
    events
    Type
    Required
    required
    Description

    The events being sent. Return a modified copy to change them.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    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.

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:

// 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:

import { Middleware } from "inngest";

// The same logic as a StaticTransform
interface PreserveDate extends Middleware.StaticTransform {
  Out: this["In"] extends Date ? Date : Jsonify<this["In"]>;
}
  • Name
    In
    Type
    Required
    required
    Description

    The original return type. Set automatically by the SDK — do not set this yourself.

  • Name
    Out
    Type
    Required
    required
    Description

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

import { Middleware } from "inngest";

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

class MyMiddleware extends Middleware.BaseMiddleware {
  declare functionOutputTransform: PreserveDate;
}

stepOutputTransform

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

import { Middleware } from "inngest";

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

class MyMiddleware extends Middleware.BaseMiddleware {
  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.

class MyMiddleware extends Middleware.BaseMiddleware {
  static onRegister({ client, functionInfo }: Middleware.OnRegisterArgs) {
    // One-time setup
  }
}
  • Name
    client
    Type
    Required
    required
    Description

    The Inngest client instance.

  • Name
    functionInfo
    Type
    Required
    required
    Description

    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.