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
| Tool | Returns to caller? | Independent execution? |
|---|---|---|
step.invoke(id, { function, data }) | Yes (awaits result) | No (caller blocks) |
step.sendEvent(...) | No | Yes (any matching fn) |
defer(id, { function, data }) | No | Yes (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.
triggersis not accepted — the function is triggered implicitly bydefer(...).schemadescribes the payload that callers send.onFailureandbatchEventsare 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.) excepttriggers,onFailure, andbatchEvents, and addsschema.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,datais validated on both the caller and receiver side and typesevent.datain the handler. Without a schema,datafalls back toRecord<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
defercan be used insidestep.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.schemawhen 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:
datais validated at the call site (synchronously).datais validated again on the receiver side. This catches serialization round-trips that change the shape (e.g. aDatebecoming an ISO string).- The same schema types
event.datain 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.