Skip to content
GitHub

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.

HotLoop nodes

  • A hotloop-flow-hotloop connection, which is a URL and an account. It signs in with POST /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 in follows HotLoop’s live feed and sends a message for each entity that changes state: msg.topic is the entity id, msg.payload is the new state, and msg.hotloop carries 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 state reads one entity when a message arrives. hotloop script runs a HotLoop script with the fields you give it. hotloop recipe applies 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 reason flow:<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 write and ethernetip write have 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 tool and mcp result serve 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 an hotloop-flow-mcp-server connection, 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.tokens can carry a rateLimit with calls and 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 an mcp.refused row with the reason. A token with no rateLimit is 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 keygen makes a key, and docs/plugins.md has 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 a helm template of the defaults is what it was.

Typed wires

  • Give the json node 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, like payload.temp: got string, want number. With no schema in the settings the node validates against msg.schema, the way Node-RED’s json node does. It is draft 2020-12 unless the schema names another draft, and a $ref to 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 inject that 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.

  • The json node used to ignore msg.schema. Now a message that carries msg.schema is 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 a msg.schema on its messages, for some other purpose, will see that.
  • A deploy can now be refused for a type mismatch (deploy.refused, reason typed wires), but only for a flow that declares schemas on its ports or sets an inject literal 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/jsonschema v6 (permissively licensed) and MCP is the Go SDK, modelcontextprotocol/go-sdk v1.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.md now 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.

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 tool node, and a call with no token or a made-up one is refused.
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.

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 https HotLoop 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 $ref outside the schema and ignores format values 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