Storage
Read, write, and delete objects; invalidate cache entries.
The storage blocks read and write JSON objects in the runtime's key-value store. They need no connector: the store is a runtime service, scoped per deployment on the platform, so a flow's objects are isolated from other deployments. The object blocks confine themselves to the user namespaces, so user keys never collide with internal runtime state (agent memory, cache entries, secrets). invalidate-cache is documented here too, but it targets cache entries rather than objects.
Key and value fields are CEL expressions evaluated against the message (body, vars, eventID, correlationID, env, now), compiled once at startup.
Picking a tier
Each of the three object blocks takes a volatile setting choosing which durability tier it operates in. invalidate-cache does not: cache entries are always volatile.
volatile | Platform | Standalone | Use it for |
|---|---|---|---|
false (default) | Postgres | serialized to --storage-dir | anything that has to survive a restart |
true | Redis | process memory | values whose loss costs a recompute |
Volatile is faster and cheaper (no database row, no transaction) but promises nothing: a volatile object may vanish on a restart, and on the platform it may be evicted while the process is still running.
An install with no Redis falls back to Postgres for volatile objects, so they cost the same as persistent ones while still offering the weaker guarantee. Write against the guarantee, not against the backend.
The two tiers are separate keyspaces. A read has to name the same tier the write used; reading the other one is a clean miss, not an error. The default is the persistent tier, so a flow written before this setting existed keeps its behavior.
object-read
Reads an object from the store into the body or a variable. When the key is absent and default is set, the default is folded in exactly like a hit; otherwise a miss nulls the body (body mode) or leaves the variable unset. Set existsVar to also record whether the key was found.
| Setting | Type | Required | Default | Description |
|---|---|---|---|---|
key | expression (string) | Yes | none | Evaluated to the object key. |
as | string | No | none | Variable to store the object under; empty replaces the body. |
default | expression | No | none | Evaluated on a miss; its result is folded in like a hit (into the variable, or the body). |
existsVar | string | No | none | Names a variable the block writes a boolean into: true on a hit, false when the default/null path was taken. Use it to tell a default apart from a stored value. |
volatile | bool | No | false | Read from the volatile tier. Must match what the write used. |
Errors: the block errors on a store failure, a key/default expression failure, or a stored value that cannot be decoded as JSON. A missing key is not an error.
- type: object-read
name: load-profile
settings:
key: '"profile:" + body.id'
as: profile
default: '{"plan": "free"}'
existsVar: profileExistsobject-write
Writes an object to the store under a key. The message passes through unchanged.
| Setting | Type | Required | Default | Description |
|---|---|---|---|---|
key | expression (string) | Yes | none | Evaluated to the object key. |
value | expression | No | message body | Value to store (JSON-encoded). Empty stores the whole body. |
volatile | bool | No | false | Write to the volatile tier instead of the persistent one. |
Writes use optimistic concurrency: the block re-reads the current version and retries on a version conflict (up to 5 attempts) before erroring. The retry re-reads the version only; it writes the value your value expression already produced from the original message and does not re-evaluate it against what the other writer stored.
Do not build a counter this way. object-read then object-write is not an atomic increment: two runs that each read 4 will each write 5, both will succeed, and one increment is lost. These blocks are for values that tolerate a lost update.
- type: object-write
name: save-profile
settings:
key: '"profile:" + body.id'
value: '{"plan": body.plan}'object-delete
Deletes an object from the store by key. The delete is unconditional and idempotent: a missing key is not an error. The message passes through unchanged.
| Setting | Type | Required | Default | Description |
|---|---|---|---|---|
key | expression (string) | Yes | none | Evaluated to the object key to delete. |
volatile | bool | No | false | Delete from the volatile tier. Must match the tier the object was written to; a delete addresses one tier, not both. |
- type: object-delete
settings:
key: '"profile:" + body.id'A volatile round trip. A fetched exchange rate may be dropped without harm, because a miss just fetches it again:
- type: object-read
settings:
key: '"rate:" + body.currency'
as: rate
existsVar: rateCached
volatile: true
# ... fetch the rate when vars.rateCached is false ...
- type: object-write
settings:
key: '"rate:" + body.currency'
value: "vars.rate"
volatile: trueinvalidate-cache
Evicts a cache-scope entry by its key so the next run recomputes. Cache entries always live in the volatile tier, so this block has no volatile setting. The key expression must produce the same value as the cache-scope's key expression: entries are stored under a hash of the evaluated key, so matching values target the same entry. The message passes through unchanged.
| Setting | Type | Required | Default | Description |
|---|---|---|---|---|
key | expression (string) | Yes | none | Evaluated to the cache key to evict; matches the key of the cache-scope that wrote the entry. |
Eviction is idempotent: invalidating a key with no entry is not an error.
- name: reset
process:
- type: invalidate-cache
name: bust-report
settings:
key: '"report"'
- type: set-payload
settings:
value: '{"reset": true}'