> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://ensforge.com/api/mcp` to find what you need.

# Workflow storage

Core accepts optional `storage`. SDK instances default to their own memory storage; React providers
created from config default to lazy IndexedDB. Explicit storage takes precedence. Passing an existing
SDK to React preserves that instance and its storage. Normal resumable ENS actions, HCA registration and independent Rhinestone
funding share one backend with separate namespaces. Storage persists progress; it does not provide
wallet custody or authorization.

```ts
import { createMemoryWorkflowStorage } from "@ensforge/core/storage";
import type { WorkflowStorage, WorkflowStoredRecord } from "@ensforge/core/storage";
```

Memory storage works in Node and browsers. Retain the adapter instance across SDK instances; it does
not survive process exit or page reload. For a browser:

```ts
import { createIndexedDbWorkflowStorage } from "@ensforge/core/storage/browser";

const storage = createIndexedDbWorkflowStorage({ databaseName: "ensforge-workflows" });
const sdk = new Ensforge({ ...config, storage });
// When this application-owned store is no longer used:
await storage.close();
```

IndexedDB opens lazily, so construction is safe during SSR; actual operations need IndexedDB.
Transactions provide atomicity across connections in the same browser origin. Storage quotas,
browser data clearing and private-browsing restrictions can affect persistence. Existing factory
imports from core/SDK roots remain supported. The neutral storage subpath does not import IndexedDB.

## Selection and resume

For normal resumable actions:

1. Explicit `resume` is used and, when it carries an ID, selects that saved instance.
2. Otherwise an explicit `workflowId` selects an existing instance.
3. Otherwise normalized action inputs, account, chain and deployment select unfinished work.
4. If no unfinished match exists, the SDK creates a new instance ID.

Conflicting explicit progress is rejected; it does not overwrite newer persisted progress. A completed
explicit instance returns its saved result. A later implicit invocation creates another instance, so
renewing a name again is distinct from retrying a prior renewal. Account/network changes cannot resume
another account's operation. Changed domain inputs select a different operation; keep inputs stable
when recovering. Confirmation and wallet transport preferences are not the operation identity.

```ts
const first = await sdk.registration.renewName({ name, duration });
const same = await sdk.registration.renewName({ name, duration, resume: first });
const history = await sdk.workflows.listWorkflows({ status: "pending", limit: 25 });
if (first.workflowId) {
  const saved = await sdk.workflows.getWorkflow({ workflowId: first.workflowId });
}
```

`workflowId` on results is optional because storage is optional. Narrow it before passing it to
`getWorkflow`. `listWorkflows` returns `nextCursor`; an account-filtered page can be empty and still
have a cursor. Custom adapters may omit enumeration, in which case listing returns an explicit error.

Supported normal flows: register/renew one or many names, wrap, transfer, set resolver and records,
create subname, migrate one or many names, and import DNS names. `setSubnameRecord` retains its existing
creation-only `resume`; the entire composite operation is not independently persisted. Ordinary
single writes and standalone HCA executions do not automatically become durable workflows.

HCA registration retains its `id` and registration-specific resume API. It can generate an ID when
shared storage is configured; legacy explicit-ID storage adapters remain compatible. Funding retains
its own ID and recovery API. These are distinct operations even when they use one backend.

## Custom database adapter

```ts
interface WorkflowStorage {
  readonly kind: "workflow-storage" | "hca-storage";
  create(input: { namespace: string; record: WorkflowStoredRecord }): Promise<boolean>;
  get(input: { namespace: string; id: string }): Promise<WorkflowStoredRecord | null>;
  compareAndSwap(input: {
    namespace: string;
    id: string;
    expectedRevision: number;
    record: WorkflowStoredRecord;
  }): Promise<boolean>;
  list?(input: {
    namespace: string;
    after?: string;
    limit: number;
  }): Promise<readonly WorkflowStoredRecord[]>;
}
```

A stored record is `{ id, revision, value }`; `value` is an opaque string encoded by the SDK.
Create succeeds only for a new `(namespace, id)` with revision zero. Compare-and-swap succeeds only
when the stored revision equals `expectedRevision`, replacing it with revision + 1. Return `false`
for a conflict and throw for storage failure. A read-then-write sequence without a database transaction
is not an atomic compare-and-swap. Return copies, not shared mutable records. Listing uses increasing
ID order and an exclusive cursor.

The [SQLite example](/hca/guides/storage) implements this with single atomic SQL statements and Node's
built-in SQLite module. Production stores must protect registration secrets, preserve namespaces,
provide tenant isolation and apply an appropriate backup/retention policy.

## Ambiguous submissions

The SDK persists submission intent before calling a wallet, then immediately saves the returned
reference. Atomic revision checks and fenced leases prevent concurrent advancement. A stale worker
cannot release a newer worker's lease. This cannot make a database and a wallet one transaction.

`SUBMISSION_UNCERTAIN` means the wallet may have broadcast. Inspect `getWorkflow().submissions`.
For a lost transaction hash, call `reconcileWorkflowSubmission({ workflowId, submissionId,
transactionHash })`. The SDK verifies actual sender, destination, calldata and value through RPC.
An arbitrary hash or an unknown wallet batch ID cannot be attached to bypass that check.

`BUSY` means another caller holds the lease; `CONFLICT` means reload progress. Invalid storage,
unknown explicit IDs and changed identity are separate errors. Do not automatically retry every error
or delete records to bypass uncertainty. HCA registration and funding use their specialized recovery
actions described in their guides.
