Deferred Functions

A deferred function is an Inngest function that runs in the background as a side effect of another run. Instead of the usual triggers, a parent run launches it by calling defer("some-id", { function, data }). The parent doesn't wait, doesn't see a result, and keeps executing. The deferred run is fully independent: its own retries, concurrency, step state, and typed payload.

Deferred functions are experimental. createDefer is imported from inngest/experimental and the API may change before GA.

defer(...) doesn't yet work with encryption middleware — payloads passed to a deferred function aren't encrypted. Support is planned; in the meantime, avoid passing encrypted data through defer(...).

Use deferred functions for work that should happen because of a run, not as part of it: scoring an agent's output, sending a notification, logging a side effect, queueing follow-up work. Any function can call the same deferred function, and each call is its own run.

When to use

ToolReturns to caller?Independent execution?
step.invoke(id, { function, data })Yes (awaits result)No (caller blocks)
step.sendEvent(...)NoYes (any matching fn)
defer(id, { function, data })NoYes (single typed target)

A common use case is an LLM scorer that runs against the output of an agent run.


Defining a deferred function

Use createDefer to define a deferred function:

import { createDefer } from "inngest/experimental";
import { z } from "zod";

export const sendEmail = createDefer(
  inngest,
  {
    id: "send-email",
    schema: z.object({ to: z.string(), body: z.string() }),
    concurrency: { limit: 5 },
  },
  async ({ event, step }) => {
    event.data.to; // typed from `schema`
    event.data.body;
  }
);

A deferred function is a regular Inngest function with its own retries, concurrency, and step state. Register it alongside your other functions in the serve handler:

serve({ client: inngest, functions: [...myFunctions, sendEmail] });

createDefer(client, config, handler): DeferredFunction

createDefer mirrors inngest.createFunction with a few differences:

  • The client is the first positional argument.
  • triggers is not accepted — the function is triggered implicitly by defer(...).
  • schema describes the payload that callers send.
  • onFailure and batchEvents are not currently supported.
  • Name
    client
    Type
    Inngest
    Required
    required
    Description

    Your Inngest client instance.

  • Name
    config
    Type
    object
    Required
    required
    Description

    Function configuration. Accepts the same options as inngest.createFunction (concurrency, throttle, rateLimit, etc.) except triggers, onFailure, and batchEvents, and adds schema.

    Properties
    • Name
      id
      Type
      string
      Required
      required
      Description

      A unique identifier for the function.

    • Name
      schema
      Type
      StandardSchemaV1
      Required
      optional
      Description

      A Standard Schema describing the payload that callers send via defer(...). Optional. When present, data is validated on both the caller and receiver side and types event.data in the handler. Without a schema, data falls back to Record<string, any>.

  • Name
    handler
    Type
    function
    Required
    required
    Description

    The async handler. Receives the same arguments as a normal Inngest function handler.


Calling defer

defer is always available on the handler context of any Inngest function:

const orderPlaced = inngest.createFunction(
  { id: "order-placed", triggers: { event: "order/placed" } },
  async ({ defer }) => {
    defer("send", {
      function: sendEmail,
      data: { to: "a@b.com", body: "hi" },
    });
  }
);

defer(...) is synchronous and fire-and-forget. The parent run continues immediately and never sees a result; the deferred run starts when the parent run finalizes. The call returns a DeferHandle whose abort() cancels the deferred run.

It also works inside step.run():

await step.run("notify", async () => {
  defer("send", { function: sendEmail, data: { to, body } });
});

defer(id, options): DeferHandle

  • Name
    id
    Type
    string
    Required
    required
    Description

    A unique identifier for this call. Must be unique within the parent run — unlike step IDs, no implicit index is appended to dedupe. (This is because defer can be used inside step.run(), where an implicit index would change on re-entry.)

  • Name
    options
    Type
    object
    Required
    required
    Description
    Properties
    • Name
      function
      Type
      DeferredFunction
      Required
      required
      Description

      The deferred function to trigger.

    • Name
      data
      Type
      object
      Required
      required
      Description

      Payload to send to the deferred function. Typed from function.schema when present.

deferHandle.abort(): void

defer(...) returns a handle scoped to that call's deferred run. Calling abort() cancels the run:

const handle = defer("send", { function: sendEmail, data: { to, body } });
// ...later in the same parent run:
handle.abort();

Like defer(...) itself, abort() is synchronous, fire-and-forget, and idempotent:

  • An abort called at any point before the parent run finalizes prevents the deferred run from starting. Deferred runs only start once the parent finalizes, so there is no window where the run has already begun.
  • It never throws into the surrounding handler. Repeated aborts, or aborting a defer(...) call that was skipped (for example, after a schema validation failure), log a warning and do nothing.
  • Other defer(...) calls in the same run are unaffected.

abort() works inside a step.run() closure: the abort ships together with the step's result, so if the result didn't reach Inngest the closure re-runs on retry and re-registers the abort.


Schemas

When schema is provided on the deferred function:

  • data is validated at the call site (synchronously).
  • data is validated again on the receiver side. This catches serialization round-trips that change the shape (e.g. a Date becoming an ISO string).
  • The same schema types event.data in the handler.

Call-site validation must be synchronous because defer(...) itself is sync. If the schema's validate returns a Promise, the SDK logs an error and the call is skipped. Receiver-side validation is async, so async validators work there.


Error handling

defer(...) is fire-and-forget, so a bad call should not derail the surrounding handler.

  • Call-site errors (for example, a synchronous schema failure) are logged via the internal logger and the call is silently skipped. The parent run continues normally and the deferred function does not fire.
  • Receiver-side errors (the deferred run reading invalid event.data, or the handler throwing) fail the deferred run itself with normal retry semantics.

Sharing across parent functions

A deferred function is one Inngest function in the backend. Multiple parents can hold a reference to it; each defer(...) call triggers an independent run with its own retries and concurrency state.