This commit is contained in:
2026-10-13 10:02:20 +02:00
parent a93f865e91
commit 80e595c02e
24 changed files with 2210 additions and 4310 deletions
+49 -75
View File
@@ -1,17 +1,18 @@
# Node types
Every type the canvas can place. Each is configured by filling in a form the
editor generates from its parameter schema, so they all behave the same way.
Anything here can also be written as a **Function** node. These types cover the
shapes that come up most often, as a form to fill in rather than repeated code.
Built-in nodes cover common integrations, timing, and logic. Configure their
settings in the node panel; use a **Function** node for your own Python.
`GET /flows/node-types` returns this list with each type's full parameter
schema, including any [connectors](#connectors) installed on your instance.
MQTT and InfluxDB need `pip install 'fluksio[server]'` on a Python installation.
The Docker image includes these dependencies. Run the broker and database
separately, or use the development stack's test services.
## Function
**`python`** — your own code, run on every incoming message.
**`python`** — run your own code when its triggering inputs arrive.
The one type with a source file. Its arguments are its input ports and its own
settings; its return value is a dict keyed by output ports. See
@@ -46,9 +47,11 @@ A topic may be a filter: `+` matches one level, `#` the rest. Everything a
filter matches lands on the same port, so use one port per topic where the
difference matters.
`json_key` is for a device that wraps its reading: Victron publishes
`{"value": 47}` on every path. Without it, a payload object is unwrapped only
when it happens to carry the port's own name as a key.
Set an explicit topic: the default `*` is a literal topic, not MQTT's
match-all filter.
For `{"value": 47}`, set `json_key` to `value`. Without it, an object is
unwrapped only if it contains the port's name as a key.
### HTTP
@@ -81,6 +84,7 @@ Outputs only makes it a **webhook**: the engine mounts a route at
| `url`, `token`, `org`, `bucket` | connection; `token` takes a secret reference |
| `write_precision` | `ns`, `us`, `ms` (default) or `s` |
| `query_range` | default window for queries, e.g. `-1h` |
| `timeout` | operation timeout in seconds; default `10.0` |
| `writes` | per-input: `measurement`, `field`, `tags` |
| `queries` | per-output: `measurement`, `field`, `tags`, `range`, `aggregation` |
@@ -96,12 +100,10 @@ Outputs only makes it a **webhook**: the engine mounts a route at
}
```
**Query passthrough.** An incoming message holding a `flux` key is run as
written, and the rows come back on the first output port as
`{"rows": [{ts, value, field, measurement, tags}], ...}`.
A database node holds the credentials and the connection, nothing else.
Building a query and shaping its rows are Function nodes on either side.
**Query passthrough.** An incoming object with a `flux` key supplies the query.
The first output receives an object with `rows`, each containing `ts`, `value`,
`field`, `measurement`, and `tags`. Use Function nodes to build queries and
shape results for charts.
### Notification
@@ -115,10 +117,10 @@ Building a query and shaping its rows are Function nodes on either side.
| `priority` | `default` | `min`, `low`, `default`, `high`, `urgent` |
| `tags` | — | comma-separated ntfy tags |
| `token` | — | for a protected topic; takes a secret reference |
| `timeout` | `10.0` | request timeout in seconds |
This is a *flow* deciding something is worth saying. The engine reporting that
it broke is [Alerts](../interface/operations.md#alerts), which is a different
thing configured elsewhere.
For engine failures rather than flow messages, configure
[Alerts](../interface/operations.md#alerts).
## Timing
@@ -135,9 +137,7 @@ thing configured elsewhere.
| `at_start` | `false` | emit once when the flow starts |
| `start_delay` | `1.0` | how long to wait before that first emission |
The scheduler: a `cron` expression here is what makes a flow run by the clock.
It is also the most-placed node in a real instance, mostly as a button someone
presses.
Use `cron` for scheduled emissions, or trigger the node by hand for testing.
### Delay & schedule
@@ -154,17 +154,12 @@ Order of operations: rate check → delay → forward. With a `cron` and no inpu
it emits the current time on each tick; with inputs it emits the last value it
received.
The engine sleeps until a delay is due rather than polling for it, so a `delay`
fires within a few milliseconds of its deadline on an idle engine, and a due
timer is taken off the queue before work that is merely waiting. It is not a
real-time guarantee: the wait ends on a normal thread, and a node that then
needs a busy cascade slot waits for one.
Timers wait until their deadline; busy workers can delay execution.
!!! note "Not in a batch flow"
A rate limit holds a value back for a timer to release, and a run has no
timer, since the value would be dropped rather than delayed. Submitting a batch
flow with a rate-limited port is refused instead.
Batch flows reject rate-limited non-streaming ports. Runs have no live
timer to release a held value.
### Trigger
@@ -181,14 +176,11 @@ starts from a value arriving. For a cron tick, see [`inject`](#inject).
| `passthrough` | `false` | send the incoming value instead of `first` |
| `wait_port` | — | an input carrying the wait, when it differs per message |
The shape this exists for: *the door opened, so turn the light on, and off
again in two minutes unless it opens again.*
For example: turn a light on when the door opens, then off after two quiet
minutes.
`wait_port` covers the case where how long to wait is itself a value: a
rollershutter takes 26 seconds up and 28 down, so the node that decides the
direction says how long to run for as well. A wait of zero or less sends
nothing afterwards, and still cancels whatever the last message scheduled,
which is how a *stop* is commanded exactly once.
Use `wait_port` when the delay comes from an input. A wait of zero or less
cancels the pending second value and schedules nothing new.
## Logic
@@ -243,8 +235,8 @@ The cheapest fix for a sensor that reports every second and changes every hour.
### Command
**`exec`** — run a command in the engine's container and read its output.
Adding one or changing its settings is a superuser's, since the command is code.
**`exec`** — run a command where the engine runs and read its output.
Adding one or changing its settings requires a superuser.
| Setting | Default | Notes |
|---|---|---|
@@ -255,11 +247,8 @@ Adding one or changing its settings is a superuser's, since the command is code.
Outputs the command's stdout, stderr and exit code.
!!! warning "Inside the container, not on the host"
A flow ported from something that read the host's journal, or poked a host
script, needs either a mount or a small listener on the host side. This
node cannot see the host.
In Docker, the command runs inside the backend container. Mount host files
explicitly if the command needs them.
### File
@@ -272,8 +261,7 @@ Outputs the command's stdout, stderr and exit code.
| `format` | `text` | `text` or `json` |
| `newline` | `true` | end each written record with a newline |
Confined to a directory the engine owns. A flow that could name any path would
be a way to read the secrets store or overwrite a node's source.
Paths stay inside the engine's files directory.
## Numeric
@@ -297,17 +285,15 @@ use a [batch flow](../concepts/runs.md) and a Function node.
**`wren`** — turn a typed command into values for the devices wired to it.
[Wren](https://github.com/stroblme/wren) is a small language model that runs
on the engine's CPU: about 40 MB, a fifth of a second per command. It reads the
house as text, the areas, the devices in each and their states, and answers a
command with calls on those devices. It is a separate package, installed into
the engine with `pip install wren`; without it the node says so on the canvas.
[Wren](https://github.com/stroblme/wren) is an optional local language model
for device commands. Install it in the engine's environment with
`pip install wren`; the node reports a missing dependency if it is absent.
| Input | Type | Notes |
|---|---|---|
| `prompt` | `str` | the command, and the only input that runs the node |
| `presence` | `str` | the area the user is in; anything else reads as away |
| `outdoor` | `float` | °C; 20 without it |
| `outdoor` | `float` or `int` | °C; 20 without it, clamped to −5…30 |
| `dark` | `bool` | without it, dark before 07:00 and from 18:00 |
The outputs are `actions`, a list of records, and any number of **targets**. A
@@ -346,43 +332,31 @@ reads.
| `apply` | `false` | write to the targets; off, the node only reports |
| `min_confidence` | `0.93` | below this nothing is written |
Each record in `actions` is `{message, value, applied, delay_s, call,
confidence}`, one per call and target. A call nothing is wired for, such as
`sensors.temperature` or a light in a room without one, gets a record with an
empty `message`. The confidence is the lowest probability the model gave any
part of its answer: at the default threshold, nine in ten right answers pass,
and 38 of the 40 wrong ones in the model's own evaluation do not.
Each record in `actions` contains `message`, `value`, `applied`, `delay_s`,
`call`, and `confidence`. Unwired calls have an empty `message` and are not
applied. Keep `apply=false` while checking how the model interprets commands.
A call with a delay ("in 10m") is held by the engine's work queue, like the
Trigger node's second value: it survives a restart where the queue is in
Redis, and not otherwise.
Every house the model was trained on had a living room with a light, media
and heating, and a light in every other area. The node fills in what is not
wired, so the model reads a house it knows; a call on one of those is
reported, never applied.
The node adds default devices to the model's view for missing expected devices.
They are reported but cannot receive writes unless wired.
!!! note "What the current model cannot do"
No levels: a light is on or off, never dimmed, coloured or half open.
Delays are whole minutes. Only one device per kind and area. Only the ten
areas above: one it was not trained on is answered confidently and wrongly,
which is why such a port is refused rather than tried. Commands are English,
phrased roughly like "turn on the lights in the kitchen".
Commands are English, such as "turn on the lights in the kitchen".
The model uses the areas above, one device per kind and area, on/off
actions, and whole-minute delays. It does not dim lights or set colors.
The chat's [Control tab](../code/agents.md#control) sends commands by hand.
## Connectors
Anything else in the palette came from an installed **connector** package: a
node type written against a published contract and discovered through the
`fluksio.node_types` entry point group. The editor shows which package supplied
it.
A connector declares its contract version, and one written for a version this
engine does not speak is ignored rather than half-loaded. Installing or
upgrading one takes effect on the next engine restart, because Python does not
re-import a changed module and a rescan would promise more than it delivers.
Installed connector packages add types through the `fluksio.node_types`
entry point group. The editor shows the source package. Incompatible contract
versions are skipped; installing or upgrading a connector requires an engine
restart.
The contract itself is in [The connector contract](connector-contract.md), and
[Writing a connector](../code/connectors.md) walks through building one.