# 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.
`GET /flows/node-types` returns this list with each type's full parameter
schema, including any [connectors](#connectors) installed on your instance.
## Function
**`python`** — your own code, run on every incoming message.
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
[Writing node code](../code/nodes.md).
Settings on a Function node are free-form: you add them by name, and they
arrive as keyword arguments.
## Integrations
### MQTT
**`mqtt`** — subscribe to topics, or publish what arrives on its inputs.
A node with *outputs only* subscribes; a node with *inputs* publishes.
| Setting | Default | Notes |
|---|---|---|
| `topic` | `*` | one topic for every port, or `{"message": "some/topic"}` per port |
| `broker_host` | `localhost` | |
| `broker_port` | `1883` | |
| `username` / `password` | — | `password` takes a secret reference |
| `client_id` | — | |
| `qos` | `0` | 0, 1 or 2 |
| `retain` | `false` | on published messages |
| `keepalive` | `60` | seconds |
| `timeout` | `10.0` | seconds before a broker operation is given up on |
| `publish_queue_size` | `256` | payloads that may wait for the broker; past this the oldest is dropped and the node reports degraded |
| `json_key` | — | key to lift out of an object payload; one for every port, or `{"port": "key"}` |
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.
### HTTP
**`http`** — receive data on a webhook, or send it to a URL.
Outputs only makes it a **webhook**: the engine mounts a route at
`/hooks/{flow}/{url}/{secret}` while the flow runs. Inputs make it a **sender**.
| Setting | Default | Notes |
|---|---|---|
| `url` | — | the route path in webhook mode, the full URL in sender mode |
| `method` | `POST` | `GET` or `POST` |
| `timeout` | `30` | seconds, sender mode |
| `headers` | `{}` | |
| `query` | `{}` | fixed query parameters; a value may be a secret reference |
| `send_inputs` | `true` | off when the inputs only trigger the request |
| `secret` | — | shared secret appended to the webhook URL; takes a secret reference |
!!! warning "A webhook with no secret is open to anyone who can reach the host."
The canvas flags this as an advisory issue rather than stopping the flow,
because a webhook on a private network is a legitimate thing to want.
### InfluxDB
**`influxdb`** — write measurements to a bucket, or read them back.
| Setting | Notes |
|---|---|
| `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` |
| `writes` | per-input: `measurement`, `field`, `tags` |
| `queries` | per-output: `measurement`, `field`, `tags`, `range`, `aggregation` |
```json
{
"writes": {
"living_temperature": {
"measurement": "environment",
"field": "temp_c",
"tags": {"room": "living"}
}
}
}
```
**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.
### Notification
**`ntfy`** — push an incoming value to a phone through [ntfy](https://ntfy.sh).
| Setting | Default | Notes |
|---|---|---|
| `server` | `https://ntfy.sh` | |
| `topic` | — | required |
| `title` | — | |
| `priority` | `default` | `min`, `low`, `default`, `high`, `urgent` |
| `tags` | — | comma-separated ntfy tags |
| `token` | — | for a protected topic; takes a secret reference |
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.
## Timing
### Inject
**`inject`** — emit a value on request, on a timer, or when the flow starts.
| Setting | Default | Notes |
|---|---|---|
| `payload` | current time | what to emit |
| `payloads` | `{}` | per-output-port payloads, keyed by port |
| `interval` | `0` | emit every n seconds; 0 means never on its own |
| `cron` | — | a five-field cron expression |
| `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.
### Delay & schedule
**`delay`** — hold messages back, limit their rate, or emit on a schedule.
| Setting | Default | Notes |
|---|---|---|
| `delay` | `0` | seconds to hold each message, fractional |
| `interval` | `0` | minimum seconds between forwards |
| `mapping` | `{}` | input port → output port; paired in order when empty |
| `cron` | — | five-field expression |
Order of operations: rate check → delay → forward. With a `cron` and no inputs
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.
!!! 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.
### Trigger
**`trigger`** — send one value now and another once things go quiet. Despite
the name, a debounce and hold rather than a scheduler: everything it sends
starts from a value arriving. For a cron tick, see [`inject`](#inject).
| Setting | Default | Notes |
|---|---|---|
| `first` | `true` | sent as soon as a value arrives |
| `then` | `false` | sent when the wait expires; empty sends nothing |
| `wait` | `60` | seconds of quiet before the second value |
| `extend` | `true` | a value arriving during the wait starts it over |
| `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.*
`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.
## Logic
### Switch
**`switch`** — send a value down one branch or another, by rule.
Each rule names an output port; a matching value leaves through that port.
Comparisons: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `contains`, `between`.
| Setting | Default | Notes |
|---|---|---|
| `rules` | `[]` | checked in order; each names the output it routes to |
| `stop_at_first` | `true` | leave through the first matching rule only |
| `otherwise` | — | output for a value that matched nothing |
Comparing a string to a number is a mistake in the rule, not a reason to take
the flow down: the rule simply does not match.
### Change
**`change`** — scale, offset, round or map a value on its way past.
| Setting | Default | Notes |
|---|---|---|
| `scale` | `1.0` | multiply numbers by this |
| `offset` | `0.0` | then add this |
| `round_to` | — | decimal places |
| `mapping` | `{}` | replace a value with another, looked up as text |
| `default` | — | value when the lookup misses; empty passes it through |
### Filter unchanged
**`rbe`** — pass a value on only when it has actually changed.
| Setting | Default | Notes |
|---|---|---|
| `deadband` | `0.0` | ignore numeric changes smaller than this |
| `deadband_percent` | `false` | read the deadband as a percentage |
The cheapest fix for a sensor that reports every second and changes every hour.
### Join
**`join`** — gather several inputs into one object or list.
| Setting | Default | Notes |
|---|---|---|
| `mode` | `object` | `object` or `array` |
## System
### Command
**`exec`** — run a command in the engine's container and read its output.
| Setting | Default | Notes |
|---|---|---|
| `command` | — | the command to run |
| `append_payload` | `false` | add the incoming value as one argument |
| `timeout` | `30` | seconds |
| `fail_on_error` | `false` | treat a non-zero exit as a node failure rather than output |
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.
### File
**`file`** — read a file into the flow, or write one out of it.
| Setting | Default | Notes |
|---|---|---|
| `path` | — | relative to the engine's files directory |
| `mode` | `read` | `read`, `write` or `append` |
| `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.
## Numeric
### Perceptron
**`mlp`** — a small neural layer over its numeric inputs.
`output = weights @ inputs + biases`, with weights drawn from `seed` so a node
reloads identically.
| Setting | Default | Notes |
|---|---|---|
| `seed` | `0` | |
A worked example of numeric logic, not a modelling tool. To train something,
use a [batch flow](../concepts/runs.md) and a Function node.
## Language
### Wren
**`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.
| 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 |
| `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
target is an output named `_` and bound to the message its device
listens on: `living_room_setpoint` bound to `home.setpoint` is the living
room's heating. The wiring is the allowlist, so the model can write nothing
that is not wired to the node.
- **area**: `living_room`, `kitchen`, `bathroom`, `bedroom`, `office`,
`garden`, `patio`, `garage`, `driveway`, `balcony`
- **kind**: `light`, `lamp`, `brightness` for a light; `setpoint`, `heating`,
`heat`, `boost` for heating; `media`, `music`, `tv`; `shutter`, `blind`,
`cover`; `water`, `watering`, `sprinkler`
A port naming anything else is refused when the flow is built. A port is
named after its message unless it is given a name of its own, in the node
panel's port name field or as `{"name": "setpoint", "port":
"living_room_setpoint"}` through the flow API.
Targets of one area and kind are one device to the model, and each takes what
a call means for its type. A target's current value is the state the model
reads.
| Call | `bool` | number | `str` |
|---|---|---|---|
| `lights.on` / `off` | `true` / `false` | `100` / `0` | `on` / `off` |
| `media.play` / `stop` | `true` / `false` | — | `play` / `stop` |
| `heating.set` | — | the temperature | — |
| `heating.boost` / `off` | `true` / `false` | — | `boost` / `off` |
| `shutters.up` / `down` | `true` / `false` | `100` / `0` | `up` / `down` |
| `shutters.stop` | — | — | `stop` |
| `watering.start` / `stop` | `true` / `false` | — | `on` / `off` |
| Setting | Default | Notes |
|---|---|---|
| `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.
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.
!!! 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".
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.
The contract itself is in [The connector contract](connector-contract.md), and
[Writing a connector](../code/connectors.md) walks through building one.
## See also
- [Payload types](payload-types.md) — what a port may carry
- [Writing node code](../code/nodes.md) — the Function node
- [The flow editor](../interface/flow-editor.md) — placing and configuring them