A run forgets everything when it ends. To carry a value from one run to the
next — the previous result of a lab test, an alert already sent, a counter —
use the workflow memory: a durable store of typed values, attached to the
subject of the run (a patient, a stay…), or to the workflow for all its
subjects. Three nodes work with it, in the
Mémoire category of the palette: Mémoriser une valeur (memory.write),
Lire une valeur mémorisée (memory.read) and Oublier une valeur
mémorisée (memory.forget).
The memory is kept in the platform database, next to the workflow definitions: restarts, deploys and new versions of the workflow do not lose it.
Choose the address of the value
Every memory node addresses one item with three settings:
| Setting | Meaning |
|---|---|
Portée (scope) |
workflow (default): the item belongs to this workflow, for the run's subject — its other versions see it, other workflows do not. subject: the item is shared by every workflow of the same subject, under the same key. workflow-global: the item belongs to this workflow, for all subjects — every run of the workflow sees the same item, whatever its subject. |
Sujet (subject) |
The context kind of the subject, e.g. patient. The id comes from the run: two patients never share an item. The kind must be established by the trigger. Not set with workflow-global, which needs no subject: a scheduled workflow, which has none, uses that scope. |
Clé (key) |
The name of the item, e.g. creatinine-precedente. It can be exposed as a port to compute it per run or per loop item (see Wire ports). |
Pick workflow unless several workflows must really share the value: a
private item cannot be overwritten by another workflow by mistake. Use
workflow-global for state that belongs to the workflow rather than to one
subject — the date of the last batch processed, a counter across patients.
Every run of the workflow writes to that one item, possibly at the same
moment: update it with a conditional write (below).
Store a value
Add Mémoriser une valeur, set its address, and wire the value to store into
its value input. Declare its type (type): a primitive (Decimal,
String, Boolean, DateTime…), an object type (Observation,
CodeableConcept…), or a list (Observation[]). In the editor, wiring
value first fills the type from the wire; a declared type is never
replaced.
memoriser:
type: memory.write
config:
scope: workflow
subject: patient
key: creatinine-precedente
type: Observation
ttl: 48 # retention: both fields, or neither (30 days)
ttlUnit: hours # seconds, minutes, hours or days
The value is checked against the declared type before it is stored: a
value that does not conform fails the node with a data error naming the item.
The type is stored with the value, explicitly, so a later read knows what was
written. Writing again under the same address replaces the value and gives it a
new version, emitted on the node's version output. A value is limited to
256 KiB of JSON; the memory is for small working state, not for documents.
Read it back
Add Lire une valeur mémorisée with the same address and a type. Its outputs:
valueandwrittenAt— emitted when an item exists;version— always emitted: the version of the item,0when there is none;missing— emitted only when there is none (never written, forgotten or expired);convertedFrom— emitted only when the value was converted (below).
Wire the "found" branch from value and the "nothing yet" branch from
missing; absence propagation stops the other one, without a guard (see
Branching and absence).
The item is checked again on read. When it was written with a type you can
read directly — the same, or a wider one (an Integer reads as a Decimal) —
it is returned as stored. When it was written with another type but a
conversion is registered between the two (the conversions offered by
Convertir (From/Into), contributed by plugins), the value is converted
automatically, element by element for lists, and convertedFrom names the
stored type. Otherwise — a Decimal read as an Integer, a conversion that
fails, a value that does not conform — the node fails instead of looking like
"nothing in memory". Forget the old item (below) to start again.
A read node has no data input: to read after a write in the same run, wire the
writer's done into the reader's after (see
Sequence with after and done).
Update safely: conditional write
Two runs of the same subject — or any two runs, for a workflow-global
item — can run at the same moment. The workflows that start on the same event
run in parallel, in no guaranteed order: when they share a subject-scoped
item, their writes to it happen in no guaranteed order either. If two runs read
a value, compute, and write it back, the second write silently overwrites the
first. To prevent it, tick Conditional write (« Écriture conditionnelle »)
on the write node (conditional: true). The node gains an expectedVersion
input and a conflict output:
- wire the reader's
versionintoexpectedVersion— the write happens only if the item still has that version; - otherwise nothing is written and
conflictis emitted, with no error: wire it to what should happen then (retry later, notify, stop).
connections:
- from: precedente.version # version read at the start of the run
to: memoriser.expectedVersion # write only if nobody wrote since
- from: memoriser.conflict # someone did
to: memoire_modifiee.after
The wire also orders the nodes: the write waits for the read, so it can never
overwrite the value before it was read. The write's done fires in both cases;
to act only after a successful write, wire from its version output instead.
When the upstream branch is skipped, expectedVersion is absent and the node
is skipped: a conditional write never falls back to an unconditional one.
Do something once: expected version 0
Version 0 means "nothing stored". A conditional write with a fixed
expectedVersion of 0 succeeds only for the first run that gets there —
until the item expires or is forgotten. That is how to send an alert once per
patient and per day, even when two events arrive together:
reserver:
type: memory.write
config: { scope: workflow, subject: patient, key: alerte-envoyee, type: Boolean,
conditional: true, ttl: 24, ttlUnit: hours }
inputs: { expectedVersion: 0, value: true }
connections:
- from: reserver.version # emitted only to the run that reserved the key
to: alerte.after
content/clinical-demo/workflows/drafts/zz-memoire-creatinine.workflow.yaml
is a complete example: each creatinine result is compared with the previous
one stored for the patient during 48 hours, a rise of at least 26.5 µmol/L is
recorded as an alert, and the new value is written only if no other run
changed the memory in between. It is a draft; launch it from the event
composer of the Executions screen.
Let a value expire
Every item expires: 30 days after its write by default, or after the
retention set on the write (ttl + ttlUnit, Conservation in the editor,
up to 100 years). Past that delay the item reads as missing (version 0),
and the platform deletes it within ten minutes. Keep retentions short whenever
the value only matters for a while: the memory holds health data.
Forget a value
- In a workflow, add Oublier une valeur mémorisée with the item's address.
- From the console, open a run in Executions: the record pane lists the
memory of the run's subjects and the workflow's
workflow-globalitems (key, version, scope, type, value, who wrote it, when, and its expiry) with an Forget button per item.
Test with simulation
A simulation never writes to the memory: writes and forgets appear in its
captured effects (port: memory), and stay readable for the rest of the
simulated run. The simulation reads the workflow's real workflow-global
items as a starting point and, when you fill the simulation's Subject
field, that subject's real memory too, versions included, whether or not its kind
has a record port. Through the
API, options.memory seeds the simulated memory explicitly — each item may
carry its version — and wins over the real one (see
Simulate a workflow).
Limits
- An unconditional write does not look at what is stored: between concurrent runs, the last write wins. Use a conditional write when that matters.
- An agent cannot use memory nodes as tools: a tool call carries no node configuration, hence no address.
Related
- Core primitives — memory for the exact ports and configuration.
- Monitor executions — where the record pane shows the memory.
- Workflow memory storage for operators.