Crux
API Reference@use-crux/core

Effects

Custom effects, receipts, recovery, rollback boundaries, reconciliation, and durable Runtime store records.

import {
  effect,
  recover,
  rollbackOnError,
  rollback,
  reconcileEffect,
  EffectOutcomeUnknownError,
  RollbackError,
} from "@use-crux/core/effect";

Effects make external state changes explicit. Every execution records an immutable receipt, recoverable definitions register one recovery unit per receipt, and rollback boundaries recover completed units in causal LIFO order.

Without a Runtime store, receipts and recovery state are process-local. With a Runtime store that implements the Effects port, those records persist and a later process can reconstruct the exact reverse recovery plan. Recovery still runs only when application code calls recover(), rollback(), or reconcileEffect() with a matching Runtime program. An external worker that claims and drives Effect recovery after process kill is not part of this surface.

effect()

Define a typed callable effect.

function effect<TOutput, TCaptured, TInput = void>(
  id: string,
  execute: EffectExecutor<TInput, TOutput>,
  options: CapturedRecoverableEffectOptions<
    TInput,
    TOutput,
    TCaptured
  >,
): RecoverableEffectDefinition<TInput, TOutput>;

function effect<TOutput, TInput = void>(
  id: string,
  execute: EffectExecutor<TInput, TOutput>,
  options: RecoverableEffectOptions<TInput, TOutput>,
): RecoverableEffectDefinition<TInput, TOutput>;

function effect<TOutput, TInput = void>(
  id: string,
  execute: EffectExecutor<TInput, TOutput>,
  options?: EffectOptions<TInput>,
): EffectDefinition<TInput, TOutput>;

Passing a recover function or captured-recovery object returns a RecoverableEffectDefinition. The default version is 1. The tuple (id, version) must identify one definition object; a conflicting definition throws EFFECT_DUPLICATE_ID.

ArgumentDescription
idStable dotted domain identifier.
executeExternal change. Receives typed input plus idempotencyKey, receiptId, scope, and optional cancellation.
options.versionReplay and recovery contract version.
options.resourceSafe resource identity projected before execution.
options.recoverRecovery function, or { capture, execute } for pre-state recovery.

The returned definition is callable and exposes id, version, _tag, and .run(). Calling it returns only the executor output. .run() returns { output, receipt }. Recoverable definitions also expose .recover().

const archiveAccount = effect(
  "account.archive",
  async (
    input: { accountId: string },
    { idempotencyKey },
  ) => accounts.archive(input.accountId, { idempotencyKey }),
  {
    resource: ({ accountId }) => ({
      type: "account",
      id: accountId,
    }),
    recover: async ({ input, idempotencyKey }) => {
      await accounts.restore(input.accountId, { idempotencyKey });
    },
  },
);

const execution = await archiveAccount.run({
  accountId: "acct_123",
});

Resource projection and capture run before the executor. If either fails, Crux does not call the executor.

recover()

Recover the single unit associated with one receipt.

function recover(
  receipt: EffectReceiptRef,
  options?: RecoverOptions,
): Promise<RecoveryUnitResult>;

options accepts reason, conflict ("fail" by default or "force"), and an AbortSignal. Recovery returns a settlement instead of erasing the original receipt. Repeated calls after success return already_recovered; ambiguous outcomes return ambiguous without invoking the handler.

const result = await recover(execution.receipt, {
  reason: "The account owner cancelled",
});

For a recoverable definition, archiveAccount.recover(receipt, options) first checks that the receipt belongs to that definition.

rollbackOnError()

Run work inside an automatic rollback boundary.

function rollbackOnError<T>(
  run: (scope: RollbackBoundaryController) => Awaitable<T>,
  options?: RollbackOnErrorOptions,
): Promise<T>;

The default { recovery: "required" } rejects an effect without recovery before it executes. { recovery: "best-effort" } admits irreversible effects and reports them honestly if rollback is later required.

If the callback returns, the function returns that value without recovery. If the callback throws and rollback completes, the original error is rethrown. If rollback is incomplete, RollbackError preserves the callback error as cause.

The controller exposes:

MemberDescription
refJSON-safe EffectScopeRef for delayed rollback.
rollback(options?)Start manual rollback and return RollbackResult.
await rollbackOnError(async (boundary) => {
  await archiveAccount({ accountId: "acct_123" });
  return boundary.ref;
});

rollback()

Recover completed units owned by an existing boundary.

function rollback(
  scope: EffectScopeRef,
  options?: RollbackOptions,
): Promise<RollbackResult>;

The result contains the scope, aggregate status, timestamps, and one RecoveryUnitResult per planned unit. Aggregate status is one of completed, partial, not_possible, failed, or cancelled.

const result = await rollback(scope, {
  reason: "Approval was revoked",
});

Rollback is sequential and causal. Repeated and concurrent requests join or reuse unit settlement so a successful recovery handler runs once.

Run-like boundaries

Flow runs and pipeline, agent, and composition roots are passive rollback boundaries. Their result types expose effects: EffectScopeRef, so completed units can be recovered later with rollback(result.effects). A run failure, suspension, cancellation, or expiration never starts recovery automatically.

Inside a flow handler, flow.effects exposes the same reference and flow.rollback(options?) starts explicit rollback immediately. Starting it makes the boundary terminal, so later Effects reject with EFFECT_SCOPE_TERMINAL.

Without a store, run-like refs resolve only while the current process retains the receipt ledger and recovery handlers. With a Runtime Effects store, the same ref reconstructs after restart; each unit still needs an exact program target or recovery returns handler_unavailable.

Durable Runtime configuration

Configure durable Effect records through the Runtime Engine, not through @use-crux/core/effect options:

import { config } from "@use-crux/core";
import {
  createRuntimeProgram,
  node,
} from "@use-crux/core/runtime";
import { postgres } from "@use-crux/postgres/runtime";

config({
  runtime: node({
    store: postgres({ url: process.env.DATABASE_URL }),
    program: createRuntimeProgram({
      targets: [reviewFlow],
      transports: [],
      effectTargets: [updateCustomer],
    }),
    retention: { effectEnvelopes: "30d" },
  }),
});
PieceRole
storeMemory (tests), PostgreSQL, or Convex Runtime adapter exposing the Effects port.
createRuntimeProgram({ effectTargets })Immutable exact (id, version) recovery targets. No stored closures.
retention.effectEnvelopesBounded envelope pruning (default 30d). Receipt/audit metadata remains.

Missing or version-mismatched cold targets settle as handler_unavailable. An undeclared recoverable Effect remains callable and can recover in the same process through its live definition. Convex supports per-operation atomicity and crash fencing; it declares multi-operation transact() callbacks unsupported. See Durability and restarts.

reconcileEffect()

Settle an effect execution or recovery attempt whose external outcome is unknown.

function reconcileEffect(
  receipt: EffectReceiptRef,
  resolution: EffectReconciliation,
): Promise<EffectReceipt>;

A resolution is either:

{ outcome: "succeeded", output: JsonValue, reason: string }
{ outcome: "failed", reason: string }

reason is retained for audit. Reconciliation rejects settled, missing, or mismatched receipt identities rather than rewriting them. Confirming a successful execution activates its prepared recovery unit; confirming failure removes it.

EffectOutcomeUnknownError

Classify an external operation whose outcome cannot be determined safely.

new EffectOutcomeUnknownError(
  message: string,
  details?: Readonly<Record<string, unknown>>,
  options?: { cause?: unknown },
);

Throw this only when retrying could duplicate an external change. Crux records EFFECT_OUTCOME_AMBIGUOUS, leaves the details on the thrown error, and requires reconcileEffect() before recovery can proceed.

RollbackError

Signals that a required rollback did not complete.

class RollbackError extends CruxEffectError {
  readonly result?: RollbackResult;
  readonly recoveryError?: unknown;
}

result contains honest unit settlements when planning completed. recoveryError retains a recovery-system failure that occurred before or during settlement, and cause retains the original callback error when one exists.

try {
  await rollbackOnError(run);
} catch (error) {
  if (error instanceof RollbackError) {
    console.error(error.result?.status);
  }
}

Errors and supporting exports

CruxEffectError is the common structured error with code, docsUrl, and cause. EFFECT_ERROR_CODES is the stable code catalog. Public contract types such as EffectReceiptRef, EffectScopeRef, EffectResource, RecoveryUnitResult, and RollbackResult are exported from the same subpath.

Learn the workflows in the Effects overview, recovery patterns, rollback boundaries, ambiguity and reconciliation, and durability and restarts. See Effects error codes for diagnosis and fixes.

On this page