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
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
76 lines
3.1 KiB
Markdown
76 lines
3.1 KiB
Markdown
# 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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```json
|
|
{"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
|
|
|
|
- [Where a node's values come from](values.md) — ports, settings and flow inputs
|
|
- [Node types](../reference/node-types.md) — the built-ins that do keep engine-side state
|