Workflow memory storage
The workflow memory holds the values that memory nodes (memory.write,
memory.read, memory.forget) keep from one run to the next, attached to a
subject of the run. Authoring is covered in
Remember values between runs;
this page describes the storage an operator runs.
Source: apps/api/src/server/memory-store.ts (store), apps/api/src/restate/memory-client.ts
(engine side), libs/shared/src/domain/memory.ts (contract).
Database
The memory lives in the application database, the PostgreSQL database of
the workflow definitions and EAI exchanges, selected like them by
DEFINITION_STORE_URL, or RUN_DB_URL when it is a PostgreSQL URL (see
Environment variables). There is no separate
setting and no other driver. The table is created at proxy boot, under the same
advisory lock as the other schemas.
CREATE SEQUENCE IF NOT EXISTS workflow_memory_version_seq;
CREATE TABLE IF NOT EXISTS workflow_memory (
subject_kind TEXT NOT NULL, -- context kind, e.g. patient; '' for scope workflow-global
subject_id TEXT NOT NULL, -- '' for scope workflow-global
scope TEXT NOT NULL, -- workflow, subject or workflow-global
workflow TEXT NOT NULL DEFAULT '', -- workflow slug; '' for scope subject
key TEXT NOT NULL,
type JSONB NOT NULL, -- TypeRef declared by the writing node
type_name TEXT NOT NULL, -- the same type, readable: Observation, Decimal[]…
value TEXT NOT NULL, -- the value, serialized as JSON
version BIGINT NOT NULL DEFAULT nextval('workflow_memory_version_seq'),
written_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ, -- set by every write
written_by JSONB NOT NULL, -- { workflow, run, node }
CONSTRAINT workflow_memory_scope_workflow CHECK ((scope = 'subject') = (workflow = '')),
PRIMARY KEY (subject_kind, subject_id, scope, workflow, key)
);
-- Added at boot when missing:
-- workflow_memory_scope_values CHECK (scope IN ('workflow', 'subject', 'workflow-global'))
-- workflow_memory_scope_subject CHECK ((scope = 'workflow-global') = (subject_kind = '' AND subject_id = ''))
A table created before the workflow-global scope carries an unnamed check on
the scope list (workflow_memory_scope_check): the first boot of a newer proxy
replaces it with workflow_memory_scope_values, once, and adds
workflow_memory_scope_subject. Existing rows are kept.
One row per address: a write replaces the row. The history of writes is the history of runs — each write node's observation, in the run index archive. A value is at most 256 KiB of JSON.
Versions
Every write takes a new version from workflow_memory_version_seq, shared
by the whole table. Versions only increase, with gaps; a version is never
reused, even after the item is forgotten or expires. 0 stands for "no item". A conditional write
(expectedVersion in the request) is one atomic statement:
| Expected version | Writes when |
|---|---|
| none | always (upsert) |
0 |
no unexpired item exists at the address |
n |
the unexpired item has version n |
When the write is refused, the proxy reads the current item. If it was written by the same run and the same node path, the request is the retry of a write that already happened — for instance after a crash between the commit and the journal — and it is answered as a success. Otherwise the answer is a conflict with the current version.
Who reads and writes
Only the proxy opens the database. The engine endpoint, which executes the
runs, calls the proxy on API_INTERNAL_URL — the address it already uses for
agents and scheduler ticks:
| Route | Caller | Purpose |
|---|---|---|
POST /api/internal/memory/read |
endpoint | Read one item ({ address } → { entry }). |
POST /api/internal/memory/write |
endpoint | Write one item ({ address, entry } → { written: true, entry }, or { written: false, currentVersion } on conflict); 413 above 256 KiB. |
POST /api/internal/memory/forget |
endpoint | Delete one item ({ address } → { forgotten }). |
GET /api/memory/:kind/:id |
console | List the unexpired items of a subject (scopes workflow and subject). |
DELETE /api/memory/:kind/:id?scope=&workflow=&key= |
console | Delete one item; 404 if it does not exist. |
GET /api/workflows/:slug/memory |
console | List the unexpired workflow-global items of a workflow. |
DELETE /api/workflows/:slug/memory?key= |
console | Delete one workflow-global item; 404 if it does not exist. |
Each engine call runs inside the durable step of its node, so a replayed run reads its journal and never calls the proxy again. The endpoint retries a call up to five times over about five seconds on a network error or a 5xx; past that, or on a 4xx, the node fails. A proxy outage longer than a few seconds therefore fails the runs that reach a memory node, as an unreachable FHIR server would.
Like the rest of the console API, these routes have no built-in
authentication (see Security posture):
keep the proxy behind the stack's authenticating reverse proxy, and do not
publish /api/internal/* beyond the cluster.
Expiry
Every write sets expires_at: 30 days after the write by default, or the
retention of the write node, up to 100 years. The deadline is computed with the
database clock, whichever replica writes. From that instant the item reads as
absent and is no longer listed. The proxy deletes expired rows every ten
minutes; with several proxy replicas each runs the same idempotent DELETE.
Backup
The table holds health data that no other system can rebuild, and that the running workflows rely on: include it in the backups of the application database (see Back up and restore). Losing the Restate volume does not lose the memory.