Crux
API Reference@use-crux/core

defer()

Register inline or durable work on the nearest Crux execution scope.

import {
  defer,
  CruxDeferError,
  DEFER_ERROR_CODES,
  type DeferredCallback,
  type DeferredWorkRef,
} from "@use-crux/core";

defer() has two overloads. It always registers on the nearest compatible execution scope.

Inline callback

function defer(callback: () => void | PromiseLike<void>): void;

The callback starts when its registering scope closes. Agent turns, adapter calls, tool executions, and Safety sessions install scopes automatically. Nested work therefore drains at its own primitive boundary rather than waiting for an enclosing request.

defer(async () => {
  await flushAnalytics();
});

The callback is process-local and best-effort. Callback failures are contained and observed. A retention-port failure is different: it propagates after the scope seals because the host failed to establish the acceptance guarantee.

Named Runtime target

function defer<TTarget extends RuntimeTaskTarget>(
  target: TTarget,
  input: RuntimeTaskInput<TTarget>,
): Promise<DeferredWorkRef>;

interface DeferredWorkRef {
  readonly kind: "deferred.work";
  readonly workId: string;
  readonly targetId: string;
}

The promise resolves after Runtime durably accepts the JSON-safe input. Named execution waits for logical finalization and outbox release.

const ref = await defer(sendReceipt, { orderId: order.id });

Inside an Eval cell, both overloads follow the cell's capture policy: callbacks do not execute, named work does not stage, and the attempted work becomes Eval evidence.

Integration ladder

  1. Inside a Crux primitive: no wrapper is required. Long-lived processes need no host binding for process-local work.
  2. On a freezing platform: configure host once. The binding retains an already-started primitive drain to root idle.
  3. At handler root: an ambient-capable binding gives each call a separate ephemeral invocation.
  4. Strict handler semantics: use a platform wrapper for cross-call grouping, outcome classification, strict response commit, and framework cleanup.

Config-only ambient calls intentionally do not share limits or handler outcome. Each call owns one ephemeral root.

Scope outcome

Inline work registered by a scope that closes with error, cancelled, or timeout is recorded as skipped and is not invoked. success, redirect, and not-found drain normally. The gate applies to the registering scope only.

Replayable flow bodies reject public defer() with DEFER_REPLAY_UNSAFE; use flow.defer().

Host imports

ImportApplication use
@use-crux/core/defer/nodeNode HTTP boundaries and process shutdown
@use-crux/core/defer/serverlessProvider-neutral strict wrappers, including withServerlessDefer
@use-crux/nextNext next(), withNextDefer, and withCrux
@use-crux/vercelVercel vercel() retention binding
@use-crux/cloudflareWorkers workers({ ctx }) and withCrux

Platform and adapter authors should use the host-adapter contract, not infer capability from environment variables.

Errors

CodeCause
DEFER_SCOPE_REQUIREDNo active scope and no ambient host binding
DEFER_CAPABILITY_MISSINGThe active host cannot honor the overload or retention request
DEFER_SCOPE_SEALEDRegistration arrived after the accepting scope sealed
DEFER_LIMIT_EXCEEDEDCallback or nesting bounds were exceeded
DEFER_REPLAY_UNSAFEPublic defer ran inside replayable flow execution
DEFER_TARGET_INPUT_REQUIREDRequired named input was missing
DEFER_COMMIT_FAILEDStrict named finalization failed before response commitment

See Troubleshooting for the decision checklist and exact remediations.

On this page