Files
app/docs/concepts/state.md
T
stroblmeandClaude Opus 5 bdad6d7fc2
Docs / docs (push) Successful in 37s
Playwright Tests / test-playwright (1, 2) (push) Failing after 1m35s
Playwright Tests / test-playwright (2, 2) (push) Failing after 17s
pre-commit / pre-commit (push) Failing after 2m8s
Test Backend / test-backend (push) Failing after 2m48s
Compose Smoke Test / test-compose (push) Failing after 13s
Playwright Tests / merge-reports (push) Failing after 2m25s
Make the docs state things rather than argue them
The site read as a design journal: rationale paragraphs, hedges
("deliberately", "on purpose", "genuinely"), meta-commentary about the docs
themselves, and one em-dash every ten lines carrying an aside.

Roughly twenty rationale blocks are gone or reduced to what a reader needs
in order to use the thing. Em-dashes go from 507 to 135, and what is left is
structural rather than prose: list and definition separators, table cells,
and four inside code blocks that quote what the CLI actually prints.

Also: api.example.com becomes api.fluksio.com (the emails stay, since
bootstrap.py really defaults to admin@example.com and RFC 2606 reserves it);
the mqtt table gains the two settings it had drifted behind on and inject's
wording matches the engine; llms.txt lists the two connector pages that were
in the nav but not in it; and the two device/device_policy notes now agree.

Builds clean under `zensical build --strict`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
2026-08-31 10:49:58 +02:00

3.1 KiB

Keeping state in a flow

Logic nodes are pure functions of their inputs. There is no context, no global, no handle to a store. A node is called with the values of the messages it declares and returns the values of the messages it provides.

That is deliberate: a node with hidden state cannot be run twice in parallel, cannot be replayed, and cannot be moved to another worker. But plenty of real automations need to remember something. This is how.

State is a message the node both reads and writes

A running total, a debounce timer, a last-seen reading: each is a value that survives between runs. Give it a message name, declare it as both an input and an output, and it is state:

def process(reading, total=0.0):
    return {"total": total + reading}

with reading and total as inputs and total as the output. Each run reads the total the previous run wrote.

Two things make this work rather than loop forever:

  • A node never depends on itself. The graph is built from message names, but a node providing a message it also requires is not placed downstream of itself. Publishing total does not re-run the node that wrote it.
  • The value must start somewhere. The first run has nothing to read. Declare the message as a flow input with a starting value; otherwise the flow reports self_loop_needs_initial at edit time and refuses to publish.

The port needs a default in the function signature (total=0.0 above) so the first call is well-formed even if the value is missing.

Feeding a value back between two nodes

Sometimes the value comes from a different node, such as a controller reading back what an estimator computed from its own last output. Written plainly that is a cycle, and the validator rejects it, because a graph where A waits for B and B waits for A can never start.

What is actually meant is that the fed-back value is state, not a trigger. Say so, by marking the input non-triggering:

{"name": "estimate", "port": "estimate", "dtype": "float", "trigger": false}

A non-triggering input:

  • creates no dependency, so it cannot form a cycle;
  • never makes the node wait. If the message has no value yet, the port is simply left out of the call, and the function's default applies;
  • is read fresh from state whenever the node does run, for whatever reason.

Use it for the back edge only. An input that should wake the node is an ordinary input, and marking it non-triggering would mean the node never runs at all.

What still holds engine-side state

Built-in nodes that are about time or change (rate limiting, filter-on-change, delay, cron) keep what they need in the engine's own state backend, under keys that never appear as messages. That is the engine's business, not a flow's: the line is that node code you write never reaches for state, while node types the engine ships may.

Storage and direct-I/O nodes are the other sanctioned exception, since talking to a device or a database is stateful by nature.

See also