Skip to main content
Store a run’s history in two ports, small JSON events in an EventStore and large bytes in an ArtifactStore, then reopen the same directory and fold the same state. Use this page when you write a host or a storage provider. For what the record is and why a stopped run carries on from it, read The record; this page stays at the storage layer and doesn’t run graph work, resume a run, list runs or manage a workspace. The smallest use sets a policy, opens the local storage and writes one artifact with the event that carries its reference:
examples/durable-storage.ts (excerpt)
createLocalRunStorage returns a live RunStorageBinding: one frozen record, the known secret values used for write checks, an event store and an artifact store. Every artifact operation names a namespace and run, and every event operation names a namespace and stream. A storage binding needs:
  • A directory, the storage root.
  • A namespace, 1 to 128 ASCII characters, starting with a letter or digit, then letters, digits, dots, underscores or hyphens. The same rule names streams and runs.
  • A policy, the size limits and the secret handling below.

Set limits

examples/durable-storage.ts (excerpt)
The local stores reject a value over a limit before it becomes visible. A second receipt for the same digest doesn’t charge the artifact bytes again, and accepted artifacts stay until deleteRun removes that run. Call preflightAppend with the exact event batch before a run writes its artifacts, and preflightWrite once with the complete same-run artifact batch; it returns the references that later writes must issue. Neither call changes anything or reserves a stream revision or quota, so the real append or write checks again and can still lose a later race.

Handle secrets

contentMode says how the bytes are used, and the store checks them that way:
  • exact and state bytes reject a configured known secret. JSON state also checks decoded keys and values, including application/*+json. The store never changes these bytes.
  • free-text bytes can have configured known secrets replaced before the store calculates the digest and byte size. The replacement is checked again, and the write fails if it would create another configured secret.
  • sensitive: true rejects the artifact instead of storing it.
The local artifact store checks receipt metadata and its exact serialised admission record too, and the local event store checks the complete event and segment metadata, not only the payload. The known secret values belong in the live local-store options, never in a stored record or digest; rotating them doesn’t make committed data unreadable, and the new set applies to later writes.

Reopen and fold

Open a fresh binding over the same directory and the same state folds back:
examples/durable-storage.ts (excerpt)
The record contains only safe JSON: namespace, provider identity, provider configuration digest and resolved policy. The local provider digest binds the resolved absolute storage root and its numeric limits, so moving the same bytes to another root produces a different record and loading the run fails instead of reading from a different address. The exact record is stored with the run and checked again when the run is loaded. Two namespaces can share one provider without reading or changing each other’s data. The runtime rejects unknown fields in the wrappers it owns: event envelopes, artifact references, receipts, run-start records and stored storage records. Graph, host and workspace payloads are opaque; the runtime keeps their JSON or bytes without reading meaning into them, so code that owns a payload validates what it uses. The example calls validateArtifactReference before it reads the reference inside its event. Run the file with npx tsx durable-storage.ts. It writes one 65 KB synthetic artifact, appends the event that carries its 194-byte reference, reopens, folds, and runs both public conformance kits:
Output
The reopened binding read one event and the same artifact bytes back, with the same digest, and both kits passed.

Failure

  • REVISION_CONFLICT: an append supplies the expected stream revision, and when another writer wins, the losing append gets this and no part of its batch becomes visible. A retry with the same bytes returns the revision of its earlier commit; a retry with different bytes conflicts again.
  • A read fails closed. An artifact read checks that the exact receipt was admitted for the run, then checks the bytes against it. Missing or changed data fails with a typed StorageError. A fresh local event store checks segment order, revisions and checksums when it reads.
  • A crash can leave bytes behind. Before admission, temporary or blob bytes that aren’t readable and don’t count against the quota. After admission, a verified, quota-charged artifact without an event reference. Hosts own cleanup and reconciliation; the contract doesn’t promise a transaction across two independent ports.
  • deleteRun removes only the artifacts in its scope. The event stream and its references stay in history, so later reads through them fail closed. Hosts coordinate event and artifact cleanup.

Bring another storage provider

Implement the root EventStore and ArtifactStore ports, then pass fresh provider instances to runEventStoreConformance and runArtifactStoreConformance. The framework-free kits check durable reopen, revision conflicts, namespace and run isolation, size limits, secret handling, content addressing, provider preflight, policy identity and read integrity. A provider only records and reopens data; the executor reads a stored run definition and resumes an unfinished node when asked to, so that work doesn’t belong in a provider.
examples/durable-storage.ts

Next steps

  • Safe node attempts: what one engine turn records, and how a crash mid-attempt is reconciled.
  • The record: the event log as the single source of truth, and what a resumed run repeats.
  • Supervised local runs: the runner’s store and how a killed run carries on from it.