HotLoop Flow 2.3.0 release notes
HotLoop Flow 2.3.0 is the release where Flow reaches the rest of the plant three new ways, and catches a wrong-shaped message before it does damage. It talks to HotLoop, an agent can call a flow as a tool over MCP, and a plugin is a signed WebAssembly file and not an npm package. JSON Schema now checks messages on the json node, on a node’s ports and on the wires in the editor. One change can affect a flow that already runs, and it is the first thing under What changed.
What’s new
Section titled “What’s new”HotLoop nodes
- A
hotloop-flow-hotloopconnection, which is a URL and an account. It signs in withPOST /api/auth/login, sends the bearer token HotLoop hands back, and signs in again when HotLoop says the session is gone. An operator account is enough for everything below. hotloop infollows HotLoop’s live feed and sends a message for each entity that changes state:msg.topicis the entity id,msg.payloadis the new state, andmsg.hotloopcarries the state it had before, its attributes, its quality and its timestamps. If the link drops, the badge goes red and one error reaches a Catch node for the outage, not one per retry. When it comes back, an entity that changed in the meantime is sent and one that didn’t isn’t.hotloop statereads one entity when a message arrives.hotloop scriptruns a HotLoop script with the fields you give it.hotloop recipeapplies a recipe with its parameters, and an older version if you ask for one on purpose. HotLoop checks every field and parameter, runs every write through its write gate and keeps the record. Flow sends the reasonflow:<node id>, and HotLoop records it in its write audit and its logbook.- A refused or failed call arrives at a Catch node with HotLoop’s own words and its status. Nothing that was written is undone, because that is HotLoop’s rule too.
Writes through HotLoop’s gate
modbus write,opcua write,s7 writeandethernetip writehave two new fields, Write through HotLoop and HotLoop tag (device/tag). With a HotLoop picked, the value goes to HotLoop’s write gate, which decides in its order: writes switched on, the tag armed, the value in range.- A write the gate refuses never reaches the equipment. A HotLoop that can’t be reached fails the write too. It never falls back to writing directly, because that would be a write the gate never saw.
- HotLoop’s own address, data type and scale for the tag apply, so the address and data type on the Flow node aren’t used. Clearing the HotLoop puts the node back exactly as it was. The gate takes one bool, number or text per write, so a list of registers is refused with a message that says so.
MCP, in both directions
mcp toolandmcp resultserve a flow as a tool. The tool node declares a name, a description and an input schema, Flow serves it at/mcp, and the result node sends the flow’s answer back. An agent signs in with a session or API token like anyone else, the call runs with that token’s permissions, and every call and refusal lands in the audit log under the caller’s name. No token, no call.mcp client, on anhotloop-flow-mcp-serverconnection, calls a tool on any MCP server, HotLoop’s own included, and uses what comes back. It never sends a call twice, because a tool may move equipment and a dropped connection doesn’t say whether it ran.- An API token in
auth.tokenscan carry arateLimitwithcallsand a window, a minute if you leave it out. A call over it never reaches the flow, comes back as a tool error that says when to try again, and writes anmcp.refusedrow with the reason. A token with norateLimitis never throttled. - It speaks the current revision of the specification, 2026-07-28, and the handshake revisions back to 2025-11-25, on the same endpoint. A flow that uses none of these nodes never makes an MCP call, and Flow serves no tool it wasn’t given.
Plugins without npm
- A plugin is a WebAssembly module, a descriptor the editor draws its dialog from, and an ed25519 signature over both. Unsigned and tampered plugins refuse to load and say why.
hotloop-flow plugin keygenmakes a key, anddocs/plugins.mdhas the rest. - A guest past its memory ceiling or its timeout traps to a Catch node while everything else keeps running. A plugin that asks for host access the operator hasn’t granted (
clock,random,context.flow,context.global) fails the deploy of any flow that uses it, before a byte is written. - With no trusted keys every plugin is refused, and with no grants every plugin that asks for host access fails. Nothing loads by default. Rust and TinyGo examples are built, signed with a throwaway key and loaded in CI.
- The chart takes all of it under
plugins, with a volume that accepts any Kubernetes volume source. Off renders nothing, and ahelm templateof the defaults is what it was.
Typed wires
- Give the
jsonnode a JSON Schema in its settings and every value it handles is checked against it. A value that doesn’t match goes to a Catch node instead of down the wire, and the error names every bad field, likepayload.temp: got string, want number. With no schema in the settings the node validates againstmsg.schema, the way Node-RED’sjsonnode does. It is draft 2020-12 unless the schema names another draft, and a$refto another URL or a file is refused, not fetched. - A node can declare what it accepts and what each output promises, in two keys in the flow file that every other tool ignores, so a flow that carries them still round-trips byte for byte. When both ends of a wire declare a schema and no message could satisfy both, the deploy is refused before a byte is written. The mismatches Flow can’t see ahead of time stop where they break the schema, go to Catch with the path of every bad field, and are counted in
hotloop_flow_node_schema_mismatches_total. - An
injectthat sets a literal the next node can never take, like the number 250 into a node that takes 0 to 100, is refused at deploy the same way. Timestamps,env, context and JSONata are skipped, never guessed. - The editor writes the sender’s type on each wire. It is muted when only one end has a type and red when the two can never agree, and the red comes from the runtime’s own check, so the canvas and the deploy can’t disagree. The Types section of a node’s dialog is where you set them.
That is nine more node types, 79 in all: five for HotLoop with its connection, and four for MCP with its connection.
What changed
Section titled “What changed”- The
jsonnode used to ignoremsg.schema. Now a message that carriesmsg.schemais validated against it and, if it doesn’t match, goes to a Catch node and not down the wire. A flow that happens to leave amsg.schemaon its messages, for some other purpose, will see that. - A deploy can now be refused for a type mismatch (
deploy.refused, reasontyped wires), but only for a flow that declares schemas on its ports or sets aninjectliteral into a node that declares one. A flow with no schemas anywhere behaves exactly as it did before. - The write nodes carry the two new fields, empty by default. With no HotLoop picked, a write node behaves as it did in 2.2.0.
- The schema validator is
santhosh-tekuri/jsonschemav6 (permissively licensed) and MCP is the Go SDK,modelcontextprotocol/go-sdkv1.8.0. Both are new Go modules, so the license text the image carries in/usr/share/doc/hotloop-flow/has more entries. The WebAssembly runtime was already in 2.2.0. - Three durable queue fixes. A removed durable wire now refuses a late write and counts it as dropped, and removing one no longer waits the whole close timeout on a target that is stuck in a message. Before, a message handed back after the wire was gone stayed charged to the quota until a restart. A drop-oldest wire no longer evicts a message that was already acknowledged or that the pump had just read, which gave the same space back twice and left the queue’s depth and the quota below what was on disk. The durable quota text in
docs/durable/README.mdnow says a diverted message can put the quota a few messages over for a moment. - The README and
docs/bench/carry the Raspberry Pi 5 results, and the roadmap checks off the Pi 5 and leaves the Pi 4, the Zero 2 W and the SD card writes open.
Upgrade notes
Section titled “Upgrade notes”From 2.2.0 it’s a plain image bump (helm upgrade, or the new tag in your Quadlet unit). No migration, no flag, nothing new on first start. A flows.json and a config file that ran on 2.2.0 run on 2.3.0, and the byte-exact round trip of the flow file is untouched.
- Going back to 2.2.0 is safe for any flow that doesn’t use the new nodes or keys. A HotLoop, MCP or plugin node fails to start on 2.2.0 (unknown node type), and a write node with a HotLoop picked writes straight to its equipment there, because 2.2.0 doesn’t read the field. Take the HotLoop off those write nodes before you go back.
- HotLoop records the account Flow signs in as. The API gives a client no way to name its own actor, so the Flow node id goes in as the reason and the account is the one on the connection. Use an account made for Flow if you want its writes easy to find.
- MCP is a new way into Flow, and it takes a token. Nothing is served until a flow has an
mcp toolnode, and a call with no token or a made-up one is refused.
New settings
Section titled “New settings”| YAML | Environment | Default |
|---|---|---|
plugins.dir |
HOTLOOP_FLOW_PLUGINS_DIR |
empty, which is plugins in data.dir |
plugins.trustedKeys |
HOTLOOP_FLOW_PLUGIN_TRUSTED_KEYS |
none. Each is ed25519:<base64>, a PEM public key, or a file holding one. With none, every plugin is refused. |
plugins.grants |
HOTLOOP_FLOW_PLUGIN_GRANTS |
none. A plugin’s name to clock, random, context.flow, context.global. The variable is name=cap,cap;name=cap. |
plugins.maxMemoryMiB |
HOTLOOP_FLOW_PLUGIN_MAX_MEMORY_MIB |
64, from 1 to 4095 |
plugins.maxTimeout |
HOTLOOP_FLOW_PLUGIN_MAX_TIMEOUT |
5s |
auth.tokens[].rateLimit.calls, .per |
0 calls, which is no limit. per is 1m when you set calls and not per. |
The HotLoop and MCP connections are set in the editor: URL, account and a timeout. The chart has all the plugin settings under plugins in values.yaml, with enabled: false, and nothing else new.
What isn’t proven
Section titled “What isn’t proven”It has never run on a real plant floor.
- HotLoop is tested against the real thing: the published HotLoop IoT 4.18.10 image runs in CI with a login and writes on, next to the Modbus server. A write through the gate lands in the register, and a write outside the tag’s range, to a tag that isn’t armed, to a tag HotLoop doesn’t have, or with HotLoop unreachable all fail with the gate’s words and leave the register where it was. Only the Modbus write was run through the real gate. The OPC UA, S7 and EtherNet/IP write nodes carry the same two fields and send through the same code, and a test checks each one offers the gate.
- The HotLoop reconnect and the diffing of the live feed are tested against a small stand-in, because dropping a real HotLoop’s connection on cue isn’t something I can do. The connection trusts the system’s certificate roots, so an
httpsHotLoop with a private CA isn’t covered. - MCP is tested against the Go SDK’s own client over real HTTP, the MCP project’s Inspector, and a real HotLoop 4.18.10 on the client side. Not done: OAuth (a bearer token is the only way in, either direction), stdio servers, resources, prompts, sampling and a tool’s output schema. HotLoop 4.18.10 speaks the handshake revisions, so against a server of the current revision the client node is tested only with Flow’s own.
- Plugins have never run on a Pi. The traps and the refusals are tested against guests built from Rust, TinyGo and Go, and a signed plugin has run in a real cluster, but the per-call numbers are from an x86 box.
- The JSON Schema validator is not Ajv. It refuses a
$refoutside the schema and ignoresformatvalues it doesn’t know. - Flow now has numbers from a real Raspberry Pi 5, measured on 2026-10-22 and published in
docs/bench/: start time, memory and throughput, on the 2.2.0 arm64 image. The board throttled under a ten minute load, and the page says by how much. A Pi 4, a Zero 2 W and SD card writes per hour on a card are still not measured. - Everything from 2.2.0 that wasn’t proven is still not proven: the S7 and EtherNet/IP nodes have only met simulators.
Image: ghcr.io/hotloop-io/hotloop-flow:2.3.0, linux/amd64 and linux/arm64. Chart: https://hotloop.io/hotloop-flow/. The README at this tag has the rest: https://github.com/HotLoop-io/hotloop-flow/blob/v2.3.0/README.md