Appearance
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.
Callout: Deferred functions are experimental. createDefer is imported from inngest/experimental and the API may change before GA.
Callout: 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:
ts
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:
ts
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.
client(Inngest): Your Inngest client instance.
config(object): Function configuration. Accepts the same options as inngest.createFunction (concurrency, throttle, rateLimit, etc.) except triggers, onFailure, and batchEvents, and adds schema.A unique identifier for the function.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>.
handler(function): 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:
ts
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. It returns void and the parent run continues immediately. The deferred run starts when the parent run finalizes.
It also works inside step.run():
ts
await step.run("notify", async () => {
defer("send", { function: sendEmail, data: { to, body } });
});defer(id, options): void
id(string): 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.)
options(object): The deferred function to trigger.Payload to send to the deferred function. Typed from function.schema when present.
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.