# `defer`

> Run a typed follow-up function in the background, without making the current run wait.

`defer()` schedules another function to run after the current run finishes. The deferred function has its own steps and retries, so follow-up work does not delay the parent run.

They are useful when:

- You need to send a notification after the main work finishes.
- You want to score or record an outcome in a separate run.
- The current run does not need the follow-up function's result.

> **Callout:** Deferred functions are in beta and available in the TypeScript SDK and in the Python SDK release after 0.5.19. createDefer is imported from inngest/experimental (create\_defer from inngest.experimental in Python), and the API may change before GA. The Go SDK doesn't support them.

## Start follow-up work

Define the target with `createDefer` from `inngest/experimental`. Register it alongside your other functions in `serve()`, then call `defer` from an Inngest function handler. Add a schema when you want the SDK to type and validate the payload.

```typescript {{ title: "TypeScript" }}
import { createDefer } from "inngest/experimental";
import { z } from "zod";
import { inngest } from "./client";
import { sendEmailMessage } from "./email";

export const sendEmail = createDefer(
  inngest,
  {
    id: "send-email",
    schema: z.object({ to: z.string().email() }),
  },
  async ({ event, step }) => {
    await step.run("send", () => sendEmailMessage(event.data.to));
  }
);

export const orderPlaced = inngest.createFunction(
  { id: "order-placed", triggers: { event: "order/placed" } },
  async ({ event, defer }) => {
    defer("send-confirmation", {
      function: sendEmail,
      data: { to: event.data.email },
    });
  }
);
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
import inngest
import pydantic
from inngest.experimental import create_defer

from .client import inngest_client
from .email import send_email_message

class SendEmailInput(pydantic.BaseModel):
    to: str

@create_defer(inngest_client, fn_id="send-email")
async def send_email(ctx: inngest.Context) -> None:
    # Python has no defer schema option; validate the payload here.
    data = SendEmailInput.model_validate(ctx.event.data)
    await ctx.step.run("send", send_email_message, data.to)

@inngest_client.create_function(
    fn_id="order-placed",
    trigger=inngest.TriggerEvent(event="order/placed"),
)
async def order_placed(ctx: inngest.Context) -> None:
    ctx.defer(
        "send-confirmation",
        function=send_email,
        data={"to": ctx.event.data["email"]},
    )
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

The parent keeps running. Inngest starts the deferred run when the parent finalizes. Give each `defer` call a unique ID within its parent run.

## API

```ts {{ title: "TypeScript" }}
const handle = defer(id, { function, data });
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
handle = ctx.defer(defer_id, function=function, data=data)
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

- **`id`**: a stable ID for this call.
- **`function`**: the function created with `createDefer`.
- **`data`**: the input. It's checked against the function's schema, and an invalid call is logged and skipped.
- **Returns** a handle. Call `handle.abort()` if the follow-up work is no longer needed.

A deferred function has its own retries and flow control. Register it in `serve()` alongside the parent. See the [`defer` reference](/docs-markdown/reference/typescript/v4/functions/deferred-functions) for every option.

## Using the right primitive

- Use `step.invoke` when the current run needs another function's result.
- Use `step.sendEvent` when an event should trigger any matching functions.
- Use `defer` when one named function should run independently with a typed payload.

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

See [`step.invoke`](/docs-markdown/durable-execution/primitives/step-invoke) and [`step.sendEvent`](/docs-markdown/durable-execution/primitives/step-sendevent).

## Define a deferred function

A deferred function is a regular Inngest function. It gets its own retries, concurrency, and step state, so its handler can use `step.run()`, `step.sleep()`, `step.waitForEvent()`, and the other step tools.

`createDefer` differs from `inngest.createFunction` in a few ways:

- The client is the first argument.
- You don't declare `triggers`. A `defer(...)` call triggers the function.
- It adds `schema`, which describes the payload callers send.
- It accepts the other `createFunction` options, such as `concurrency`, `throttle`, and `rateLimit`. `onFailure` and `batchEvents` are not supported yet.

```ts {{ title: "TypeScript" }}
import { createDefer } from "inngest/experimental";
import { z } from "zod";
import { inngest } from "./client";

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;
  }
);
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
import inngest
import pydantic
from inngest.experimental import create_defer

from .client import inngest_client

class SendEmailInput(pydantic.BaseModel):
    to: str
    body: str

@create_defer(
    inngest_client,
    fn_id="send-email",
    concurrency=[inngest.Concurrency(limit=5)],
)
async def send_email(ctx: inngest.Context) -> None:
    data = SendEmailInput.model_validate(ctx.event.data)
    print(data.to, data.body)
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

Register it in your serve handler alongside your other functions:

```ts {{ title: "TypeScript" }}
import { serve } from "inngest/next"; // or your framework's adapter

// myFunctions: the other Inngest functions your app already serves
serve({ client: inngest, functions: [...myFunctions, sendEmail] });
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
import fastapi
import inngest.fast_api

from .client import inngest_client
from .define_deferred import send_email

app = fastapi.FastAPI()

# my_functions: the other Inngest functions your app already serves
inngest.fast_api.serve(app, inngest_client, [*my_functions, send_email])
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

## Call `defer`

`defer` is on the handler context of every Inngest function. The call is synchronous and fire-and-forget. The parent continues at once and never sees a result. Inngest enqueues the deferred run when the parent run finalizes, not when you call `defer`.

You can also call `defer` inside `step.run()`:

```ts {{ title: "TypeScript" }}
// Inside a handler that destructures { defer, step }; `to` and `body` are your payload values
await step.run("notify", async () => {
  defer("send-confirmation", { function: sendEmail, data: { to, body } });
});
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
# `to` and `body` are your payload values
async def notify() -> None:
    ctx.defer(
        "send-confirmation",
        function=send_email,
        data={"to": to, "body": body},
    )

await ctx.step.run("notify", notify)
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

The ID must be unique within the parent run. Unlike step IDs, Inngest does not append an index to tell duplicate IDs apart. A call with a duplicate ID is logged and skipped.

## Type and validate the payload

When the deferred function has a `schema`, the `data` you pass to `defer(...)` is type-checked and validated:

- `data` is validated at the call site, synchronously.
- `data` is validated again when the deferred run receives it. This catches changes from serialization, such as a `Date` becoming an ISO string. If this check fails, the deferred run fails without retries. The parent run is never affected.
- The same schema types `event.data` in the deferred handler.

Call-site validation must be synchronous because `defer(...)` is synchronous. If the validator returns a Promise, the call is logged and skipped, so use a synchronous validator. Without a schema, `data` is typed as `Record<string, any>`.

## Know which run deferred the work

You don't need to pass the parent's ID yourself. The SDK passes it through for you. Read it from `parents` on the handler context. Each entry has the parent's `fnSlug` and `runId`:

```ts {{ title: "TypeScript" }}
async ({ event, parents }) => {
  const { fnSlug, runId } = parents[0];
}
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
fn_slug, run_id = ctx.parents[0].fn_slug, ctx.parents[0].run_id
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

## Group runs with sessions

A deferred run joins the [sessions](/docs-markdown/agent-evals/sessions) of the run that deferred it. The two runs appear together in the dashboard, and you don't need to pass anything.

To set a session on the deferred run yourself, pass `meta.sessions`:

```ts {{ title: "TypeScript" }}
defer("send-confirmation", {
  function: sendEmail,
  data: { to: event.data.email, body: "Thanks for your order!" },
  meta: {
    sessions: {
      conversation_id: event.data.conversationId,
    },
  },
});
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
ctx.defer(
    "send-confirmation",
    function=send_email,
    data={"to": ctx.event.data["email"], "body": "Thanks for your order!"},
    meta={
        "sessions": {
            "conversation_id": str(ctx.event.data["conversationId"]),
        },
    },
)
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

Sessions you set here win over inherited ones. To clear inherited sessions or turn off inheritance, see the [`defer` reference](/docs-markdown/reference/typescript/v4/functions/deferred-functions#sessions) and [Sessions](/docs-markdown/agent-evals/sessions).

## Credit scores to an experiment

When the deferred function is an [LLM scorer](/docs-markdown/agent-evals/deferred-scoring), you can credit its score to the [experiment variant](/docs-markdown/agent-evals/experiments) that produced the output. Pass the `experimentRef` returned by [`group.experiment()`](/docs-markdown/durable-execution/primitives/group-experiment) as the `experiment` option:

```ts {{ title: "TypeScript" }}
// feedbackScorer is a scorer created with createScorer;
// experimentRef comes from `await group.experiment(...)` earlier in this run
defer("score", {
  function: feedbackScorer,
  data: { ticketId },
  experiment: experimentRef,
});
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
# feedback_scorer is a deferred function created with create_defer;
# experiment_ref comes from `await ctx.group.experiment(...)` earlier in
# this run
ctx.defer(
    "score",
    function=feedback_scorer,
    data={"ticketId": ticket_id},
    experiment=experiment_ref,
)
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

The deferred handler sees the variant on `parents[0].experiment` as `{ experimentName, variant }`. Later scores then credit the variant that served the result. See [Scores](/docs-markdown/agent-evals/scores) for other ways to record outcomes.

## Cancel a deferred run

`defer(...)` returns a handle. Call `abort()` on it when the parent learns, after deferring, that the work is no longer needed.

For example, a data-sync function defers an expensive LLM job to judge the results. Midway through, it finds the import was too small to be worth judging:

```ts {{ title: "TypeScript" }}
// judgeResults is a deferred function; syncData and MINIMUM_ROWS_WORTH_JUDGING are your own code
async ({ event, defer, step }) => {
  const scoring = defer("judge-results", {
    function: judgeResults,
    data: { importId: event.data.importId },
  });

  const { rowCount } = await step.run("sync-data", () => syncData(event.data));

  if (rowCount < MINIMUM_ROWS_WORTH_JUDGING) {
    scoring.abort();
  }
}
```

```python {{ title: "Python" }}
# Requires the inngest release after 0.5.19.
import inngest

# judge_results is a deferred function; sync_data and
# MINIMUM_ROWS_WORTH_JUDGING are your own code
async def sync_function(ctx: inngest.Context) -> None:
    scoring = ctx.defer(
        "judge-results",
        function=judge_results,
        data={"importId": ctx.event.data["importId"]},
    )

    result = await ctx.step.run("sync-data", sync_data, ctx.event.data)

    if result["rowCount"] < MINIMUM_ROWS_WORTH_JUDGING:
        scoring.abort()
```

Use [`step.Send`](/docs-markdown/durable-execution/primitives/step-sendevent) to trigger a follow-up function instead.

If you know the answer before you call `defer(...)`, skip the call instead. Use `abort()` when the defer is already registered, for example in an earlier step or in shared code you don't control.

- `abort()` is synchronous, fire-and-forget, and idempotent, like `defer(...)`.
- Misuse is logged and ignored, not thrown. This includes aborting twice or aborting a call that was skipped.
- A deferred run starts only after the parent finalizes. An abort anywhere in the parent run stops it from ever starting: right after the `defer(...)` call, after other steps, or inside a `step.run()` closure.
- Aborting one deferred run does not affect the run's other `defer(...)` calls.

> **Callout:** Calling abort() inside a step.run() closure is retry-safe. The abort ships with the step's result, so one can't reach Inngest without the other.

See the [`deferHandle.abort()` reference](/docs-markdown/reference/typescript/v4/functions/deferred-functions#defer-handle-abort-void) for the exact behavior.

## Handle errors

A bad `defer` call never breaks the parent run.

- **Call-site errors** are logged and the call is skipped. The parent continues, and the deferred function does not run. These include a synchronous schema failure, an invalid `meta.sessions` value, and a function that wasn't created with `createDefer`.
- **Receiver-side errors** fail the deferred run, never the parent. If the deferred run receives `event.data` that fails schema validation, the run fails without retries. If the handler throws, the run retries like any other Inngest function.

## Reuse one deferred function

A deferred function is a single function in Inngest. Many parent functions can defer it. Each `defer(...)` call starts an independent run with its own retries, concurrency, and step state.

## Sandbox cleanup

If you keep a sandbox after the main work, pass its ID to a deferred cleanup function. The parent can finish while teardown runs in its own retryable run. Make cleanup safe to repeat. `defer` is not a `finally` block; the published docs do not promise cleanup when the parent fails or is cancelled. Cover those paths separately, and use Inngest's built-in sandbox lifecycle when it already handles teardown. See [Sandboxes](/docs-markdown/sandboxes) and [cleanup after cancellation](/docs-markdown/durable-execution/guides-and-advanced/cancellation#clean-up-after-cancellation).

## Limits

- Deferred functions are currently beta and available in the TypeScript SDK and the Python SDK release after 0.5.19. Python's `create_defer` has no `schema` option; validate `ctx.event.data` in the handler.
- Invalid `defer` calls, including synchronous payload validation failures, are logged and skipped while the parent continues.
- `defer` does not yet work with [encryption middleware](/docs-markdown/durable-execution/guides-and-advanced/middleware/encryption-middleware), so deferred payloads are not encrypted. Do not send data that requires that middleware in a deferred payload. Support is planned.

## Next

- Read the [deferred functions reference](/docs-markdown/reference/typescript/v4/functions/deferred-functions) for the full `createDefer` and `defer` API.
- Read [Events and triggers](/docs-markdown/durable-execution/guides-and-advanced/events-and-triggers) for other ways to start a function.
- Read [Primitives](/docs-markdown/durable-execution/primitives) for the other ways to pause and resume work.