hotloop
GitHub
HotLoop FlowLicense: BSL 1.1

Wire the plant. Review it like code.

A flow engine for the plant floor, in one static Go binary with its own editor. Every deploy is a record you can diff and roll back, a trace shows where a message went, and a wire can ride out an outage. It reads your existing flows.json too, so you can bring what you have.

HotLoop Flow 2.8.1 is out. It is 2.8.0 with the arm64 image added, and every flow you have runs the way it did.

The code is at github.com/HotLoop-io/hotloop-flow, and the image is ghcr.io/hotloop-io/hotloop-flow, with the tags 2.8.1, 2.8 and 2, for amd64 and arm64. The chart is at https://hotloop.io/hotloop-flow/. From 2.8.0 it is a plain image bump, with no migration and no new settings. 2.5.0 is the release with the new editor, so reload the editor tab once if you are coming from 2.4.0 or earlier. 2.4.0 is where the EmberCORE dashboard started signing people in, off unless it hands Flow its key. 2.3.0 is where the json node started checking msg.schema, so coming from 2.2.0 or earlier, that is the change that can touch a flow that already runs. 2.2.0 was the first release under the HotLoop Business Source License 1.1, which every release since shares: free for home and lab, and free for business through EmberNet. Version 0.1.0 was published as Emberwire, and 2.0.0 does not read what the old names wrote. Read the 2.8.1 notes before you upgrade.

What Flow does

Ship it, watch it, and let it ride out a bad hour.

Each card is the short version, and the chips say which release a feature came in. The full text for each is in the block under the cards, and in the docs.

Ship changes safely

Every deploy is a record, and a flow can be tested before it ships.

  • Deploy records, diff and rollbackSince 2.1.0

    Who, when, why, and the flow file byte for byte, with credentials encrypted as they stood. A diff reads node by node, and rollback redeploys an old record.

  • Flows as codeSince 2.1.0

    hotloop-flow deploy prints the diff and deploys against the revision it came from. A pipeline holds a token that can read and deploy, nothing else.

  • Tests and a deploy gateSince 2.1.0

    A flows.test.yaml runs on the real runtime. Turn on tests.gate and a deploy that fails the suite is refused with the whole report.

  • Partial deploySince 2.1.0

    A save restarts the nodes that changed. MQTT sessions, listeners and Delay queues on everything else keep running.

See what happened

Click a message and see the path it took.

  • Tracing and replayIn 2.2.0

    The canvas lights up the path a message took, with timing and payload at every hop. Replay it into a draft of the flow, or turn the trace into a test.

  • OpenTelemetry exportIn 2.2.0

    Every hop can go to your collector as a span. It is proven against the real collector image, and the trace buffer is bounded.

  • Metrics and an audit log

    Prometheus at /metrics, per node. Logins, deploys, refusals and rollbacks go to an audit log, each saying who, from where, and what.

Ride out the bad hour

A slow historian or a chatty sensor is a graph, not an outage.

  • Bounded inboxes

    Every inbox has a ceiling and a policy per node: block, drop the newest, drop the oldest, or raise it to a Catch node. Every message that does not fit is counted.

  • Durable wiresIn 2.2.0

    Mark a wire durable and, when the node behind it stops answering, messages go to disk in order and drain when it returns. Proven against an InfluxDB killed for ten minutes.

  • Context that survives a restartIn 2.2.0

    Flow, global and node context live in SQLite with no cgo, so a restart no longer wipes them.

  • One goroutine per node

    Each node runs on its own lightweight thread, ordered within a node and parallel across nodes. A CPU-heavy Function node costs you a core, not the editor and every other flow on the box.

Talk to the plant floor

Native nodes for the protocols a plant already runs.

  • Modbus TCP and OPC UAIn 2.2.0

    Modbus read and write, and OPC UA read, subscribe, and write. Tried against pymodbus and Microsoft's OPC PLC.

  • S7 and EtherNet/IPIn 2.2.0

    Read and write, tried against simulators only. Neither has met a Siemens CPU or a Logix controller yet.

  • Sparkplug BIn 2.2.0

    As an edge node and as a host, checked against pysparkplug in both directions and against a real Mosquitto.

  • MQTT 5, TLS and Node-RED's schedulesSince 2.1.0

    MQTT 5 with properties and the last will, TLS on TCP, HTTP request and MQTT, and Inject on a crontab that fires when Node-RED's would.

Reach the rest of the plant

Talk to HotLoop, let an AI agent call a flow, and catch a wrong-shaped message early.

  • HotLoop nodes and gated writesIn 2.3.0

    Follow HotLoop's live feed, read state, run scripts, and apply recipes. The write nodes can send through HotLoop's write gate, and a refused write never reaches the equipment.

  • Flows as MCP toolsIn 2.3.0

    An MCP tool node serves a flow to an AI agent at /mcp (MCP is the standard agents use to call tools), and an MCP client node calls any MCP server. A token is required, and every call is audited under the caller.

  • Signed WebAssembly pluginsIn 2.3.0

    A plugin is a signed, sandboxed WebAssembly file, not an npm package. Unsigned ones refuse to load, and with no trusted keys nothing loads at all.

  • Typed wires and the Inject checkIn 2.3.0

    Give the json node a JSON Schema and a wrong-shaped value goes to Catch with every bad field named. A deploy is refused when two ends of a wire can never agree.

FlowLab

A second way to run a tab, for test benches and data loggers. It has 28 node types of its own, beside the 79 for messages.

  • Diagrams with loops and an error wireIn 2.7.0

    In a diagram each node runs once, when every wired input has a value, and nodes with no wire between them run at the same time. While and for loops carry a value from one pass to the next, and an error travels on a wire of its own. Flows run exactly as they did.

  • A panel and a debuggerIn 2.7.0

    A panel at /panel/<diagram id> with controls, indicators and charts that opens on a phone, and a run bar with highlight execution, probes, breakpoints and step.

  • Analysis, DAQ and TDMSIn 2.8.0

    Statistics, integral, derivative and Butterworth filter nodes, a node that reads NI hardware through NI's gRPC Device Server, a durable MQTT output, and a TDMS log. The DAQ node has met a stand-in server, and the TDMS files have not met NI's own software.

Locked down by default

It refuses to start without a login in front of it.

  • No login is a startup error

    Turning authentication off takes an environment variable, on purpose, and the editor wears a no-login badge while it is off.

  • Two-factor and lockoutsSince 2.1.0

    TOTP sign-in is free. Five wrong passwords lock an account from that address for fifteen minutes, and sessions survive a restart.

  • Credentials under GCM

    Credentials are encrypted with AES-256-GCM under an Argon2id key, and deploy records never hand them out, encrypted or not.

  • exec off, files fenced

    The exec node ships disabled until an operator allowlists commands, with no shell. File nodes are scoped to the volume, symlinks resolved.

  • A sandbox with nothing to reach

    The Function node runs on goja with no require, no process, no filesystem, and no network. Plugins run as signed WebAssembly with a memory ceiling and a timeout.

  • One static binary

    A 41.7 MB static binary that idles at about 16 MiB and answers its first request in 0.05 s. A Pi 4 and a Zero 2 W have not run it yet.

The full detail, from the release notes

New in 2.3.0. All of it is in ghcr.io/hotloop-io/hotloop-flow:2.3.0. The consequence: a flow can act through HotLoop's write gate, an agent can use a flow as a tool, and a plugin no longer means an npm package.

HotLoop nodes and gated writes

A HotLoop connection is a URL and an account. hotloop in follows the live feed and sends a message for each entity that changes, hotloop state reads one entity, and hotloop script and hotloop recipe run a script or apply a recipe. HotLoop checks every field, runs every write through its write gate and keeps the record, with the reason flow:<node id>. The Modbus, OPC UA, S7 and EtherNet/IP write nodes have a Write through HotLoop option. A write the gate refuses never reaches the equipment, and a HotLoop that can't be reached fails the write instead of falling back to writing directly.

MCP, in both directions

mcp tool and mcp result serve a flow as a tool at /mcp. 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. An API token can carry a rate limit. mcp client calls a tool on any MCP server, and never sends a call twice, because a tool may move equipment and a dropped connection doesn't say whether it ran. Nothing is served until a flow has a tool node.

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. A guest past its memory ceiling or its timeout traps to a Catch node while everything else keeps running, and a plugin that asks for host access nobody granted fails the deploy of any flow that uses it. With no trusted keys every plugin is refused.

Typed wires and the Inject check

Give the json node a JSON Schema and every value it handles is checked, with an error like payload.temp: got string, want number. Nodes can declare what they accept and what each output promises, in two keys 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. An inject that sets a literal the next node can never take, like 250 into a node that takes 0 to 100, is refused the same way. The editor draws the type on each wire, red when the two can never agree.

What changed

The json node used to ignore msg.schema and now validates against it, so a flow that leaves a msg.schema on its messages for another purpose will see that. Three durable queue fixes: a removed durable wire refuses a late write, a drop-oldest wire no longer evicts a message twice, and a late hand-back no longer stays charged to the quota. The count is now 79 node types.

New in 2.2.0. All of it is in ghcr.io/hotloop-io/hotloop-flow:2.2.0. The consequence: a flow that has to talk to a PLC, survive a restart or explain itself no longer needs a workaround.

Tracing and replay

Click a debug message and the canvas lights up the path it took, with timing and the payload at every hop, through splits, joins, and subflows. Replay a captured message into a draft of the flow and compare the output, or turn a trace into a test. The trace buffer is bounded, and OpenTelemetry export is proven against a real collector.

Durable state

Context survives a restart, in SQLite with no cgo. Mark a wire durable and its queue touches the disk only when the node behind it stops answering. A real InfluxDB was killed for ten minutes under steady input and brought back, and the rows came out none missing and none doubled. The consequence: a historian going down for an hour no longer costs you an hour of readings.

The plant floor

Modbus TCP read and write, OPC UA read, subscribe and write, S7 and EtherNet/IP read and write, and Sparkplug B as an edge node and as a host. Modbus was tried against pymodbus, OPC UA against Microsoft's OPC PLC, S7 and EtherNet/IP against simulators, and Sparkplug B against a real Mosquitto. None has met a physical PLC.

Already in, from 2.1.0. These shipped in 2.1.0 and are in 2.3.0. The consequence: "who changed the flow on line 3, and put it back" is a command, not an archaeology project.

Deploy records, diff and rollback

Every deploy keeps who, from where, when, the note, the revision it replaced, and the flow file byte for byte, with the credentials encrypted as they stood. A diff reads node by node and keeps layout apart from logic, and rollback puts an old record back as a new deploy, flows and credentials together. An optional git mirror commits every deployment under whoever deployed it.

Flows as code

hotloop-flow deploy prints the diff against what is running and deploys against the revision it came from, so a race is a red pipeline. A pipeline holds an API token that can read and deploy and nothing else, never an admin password.

Flow tests and a deploy gate

A flows.test.yaml next to flows.json runs on the real runtime and nodes, with Delay, Trigger, and Inject on a clock the test moves and the outside world stood in for. Turn on tests.gate and a deploy that fails the suite is refused with the whole report. Rollbacks are not gated.

Partial deploy

A save restarts the nodes that changed. MQTT sessions, listeners and Delay queues on everything else keep running.

Sign-in that holds up

Two-factor sign-in (TOTP) is free, lockouts are on by default, sessions survive a restart, and an audit log records who did what from where.

MQTT 5, TLS and Node-RED's schedules

MQTT 5 with properties and the last will, TLS on TCP, HTTP request and MQTT, and Inject on a crontab that fires on the same minute Node-RED would, checked across clock changes in six zones.

The editor

Wire it, review it, follow a message down the wires.

Light or dark, it follows your system. These come from the repository's own screenshot script, which starts the real binary and a real browser on an oven line with two deployments behind it, so they get retaken when the editor changes.

The Flow editor on an oven line: the canvas, the deployment history, and the debug and runtime panels The Flow editor on an oven line, in dark
The canvas is what's running. The debug dock and the Runtime table under it say what every node is doing.
The review panel listing one changed node, with a note box, beside the canvas with that node outlined The review panel in dark
Deploy shows the change on the canvas and in words, and asks for a note that lands in the history.
A traced message lit across the canvas with the time each node spent on it, and the debug dock below A traced message in dark
Click Trace on a debug line and the path that message took lights up, hop by hop.
The sign-in card The sign-in card in dark What a runtime says when the dashboard signs people in and the page was opened another way The dashboard notice in dark
Flow won't start unlocked. When the EmberCORE dashboard signs people in, a runtime opened any other way says to come in through the dashboard.
Install it

One image, one volume, and an admin account before it will start.

Flow refuses to run unlocked, so every path sets an admin password hash and a credential secret first. The image is ghcr.io/hotloop-io/hotloop-flow:2.8.1 for amd64 and arm64, with no shell in it, so hashing the password is a subcommand of the binary. Keep the credential secret somewhere that is not the box: every broker and database password Flow stores is unreadable without it.

Podman

podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.1 hash-password -password 'something-long'
openssl rand -hex 32
podman run -d --name hotloop-flow -p 1880:1880 -v hotloop-flow-data:/data \
  -e HOTLOOP_FLOW_ADMIN_USER=admin \
  -e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH='<the hash>' \
  -e HOTLOOP_FLOW_CREDENTIAL_SECRET='<the secret>' \
  ghcr.io/hotloop-io/hotloop-flow:2.8.1

Quadlet units that follow the tag, and the full steps, are on the install page.

k3s with Helm

helm repo add hotloop-flow https://hotloop.io/hotloop-flow/
helm install line3-flows hotloop-flow/hotloop-flow

The chart makes the admin password and the credential secret on first install and keeps them on every upgrade. One release is one instance, on purpose.

The numbers

A 41.7 MB binary that answers in 0.05 s, with the limits stated up front.

Linux under WSL2, 24 CPUs (a Ryzen 9 7900X), 2026-10-02, the native binary built from main, three runs of each command, and the range is what's printed. The two size rows were measured again on 2026-11-01 for 2.8.0: the binary is 41,693,346 bytes built with CGO_ENABLED=0 go build -trimpath -ldflags="-s -w", and the image row is the 2.8.0 amd64 image built from the release tree with podman build --platform linux/amd64 --build-arg VERSION=v2.8.0 . and read with podman image inspect, 45,635,783 bytes. It was not read from ghcr.io, because the tag wasn't pushed yet. The other rows are still the 2026-10-02 build. Flow's repository prints absolute numbers only, because a side-by-side table moves every time the box changes and ends up describing the box more than Flow.

MeasureFlow
Binary, linux/amd64, static41.7 MB
Image, 2.8.0 amd64, as Podman reports it45.64 MB
Start to answering its first request0.05 s
Memory in use (RSS), idle16.4 to 16.7 MiB
RSS, peak under the HTTP load26.4 to 28.5 MiB
HTTP, five-node flow, 8 connections13.0k to 15.3k req/s
HTTP latency p50 / p990.39 to 0.41 ms / 1.78 to 4.03 ms
Engine, five-node chain, no I/O2.20 to 3.11 µs a message

On a Raspberry Pi 5

The same flow and the same commands on 2026-10-22, running the published 2.2.0 arm64 image as a pod on one Pi 5 with 8 GB and four Cortex-A76 cores, its clock capped at 2.0 GHz. Three runs of 30 seconds, and the range is what's printed.

MeasureFlow on a Pi 5
Start to answering its first request0.10 to 0.46 s
RSS, idle88.0 to 88.3 MiB
RSS, peak under the HTTP load40.3 to 40.5 MiB
HTTP, five-node flow, 8 connections13.2k to 13.4k req/s
HTTP latency p50 / p990.48 ms / 2.72 to 2.78 ms
Engine, five-node chain, no I/O7.74 to 8.15 µs a message
HTTP, the same command held for 600 s11.4k req/s, p50 0.58 ms, p99 3.00 ms

The 600 second row is the honest one for a box that stays loaded. The board hit its soft temperature limit inside a minute and throttled from about two minutes in, so the clock sat at 1.5 to 1.7 GHz for the rest. Idle memory is about five times the x86 figure above, and I haven't found out why, so I'm not offering a cause. It is one board on one afternoon with a capped clock. The method and what I didn't establish are in the repo's bench docs.

Now, walking our own numbers back.

A benchmark table with no caveats under it is marketing wearing a lab coat. Here are four things these figures do not say.

  1. The HTTP number includes the whole HTTP stack, on purpose, since that is what a client of your flow sees. It is not a scheduler benchmark and should not be quoted as one. The engine row is the scheduler, with no I/O in the path.
  2. The tail moves more than the median. Under saturation every runtime queues, and queueing owns p99, so p99 wanders between runs while p50 barely does.
  3. The engine's peak heap is a burst, not a resting state. That row fires 200,000 messages at once and the bounded inboxes hold the rest back. Feed it at a sensor's pace and memory sits where the RSS rows say.
  4. WSL2 is not a Pi. The Pi 5 is measured above. A Pi 4 and a Zero 2 W have not run it yet, and the image grew from 32.1 MB at 2.1.0 to 39.1 MB at 2.2.0, 44.8 MB at 2.3.0, 44.95 MB at 2.5.0, 45.34 MB at 2.7.0 and 45.64 MB at 2.8.0. That growth came from the plant nodes, tracing, the SQLite state store, the HotLoop and MCP nodes, JSON Schema checking, the plugin host and FlowLab, and nobody has measured how much each one added.

Memory is the process's own resident memory, read from /proc. It is not what podman stats reports, which is what the kernel charges the container, and which read 2.6 MB for a process that held 14.7 MB. A figure that moves fivefold depending on the tool doesn't get printed.

Produced by the benchmark command that ships in the repository, so you can disagree with it on your own hardware. The method, the flow file, the exact commands, and the caveats live in the repo's bench docs.

Flow against Node-RED

Same flow file, both runtimes, and Node-RED loses where it counts.

I deployed the same five-node flow file to both, unchanged, in rootless Podman on one 12 CPU Linux box, and hit both with the same load generator in the same sitting: 8 connections, 30 seconds, the latest Node-RED image. Flow came out smaller, faster, and a lot quicker off the line, and Node-RED needed a 717 MB image to say hello. Then I walked my own numbers back underneath, because a benchmark with no caveats is marketing in a lab coat. The upshot: if a plant depends on it, you want the one that is still standing at 3am.

The table and every caveat behind it
MeasureFlowNode-REDRatio
Image size45.64 MB717 MB16×
Throughput3,460 req/s1,290 req/s2.7×
Latency p501.03 ms4.61 ms4.5×
Latency p9916.72 ms23.14 ms1.4×
Cold start2.1 to 2.3 s4.6 to 5.5 s~2.3×

Three things that table does not say.

  1. Cold start includes roughly two seconds of container start that both runtimes pay, so the real gap between the two programs is smaller than that row implies.
  2. The throughput figure includes the whole HTTP stack, on purpose, since that is the only surface both present identically. It is not a measurement of either scheduler in isolation, and it should not be quoted as one.
  3. The p99 gap is far narrower than the p50 gap, for a reason that flatters nobody. Under saturation both runtimes queue, queueing dominates the tail, and the median is the only column where per-request cost actually shows up.

Two more, so nobody has to find them. The throughput, latency and cold start rows are from the run on 2026-08-08, on an earlier build of Flow, so don't divide them by the newer absolute numbers above. The image row is the 2.8.0 image, built from the release tree (45.64 MB, measured as above), against the Node-RED image from that run. When I ran it, Flow was 25.1 MB and the gap was 29×, and Flow has grown since, with the plant nodes, tracing, the SQLite state store, the HotLoop, MCP and plugin work of 2.3.0, and FlowLab since.

Memory isn't in this table, on purpose. Those numbers came off podman stats, which reports what the kernel charges the container, not what the process holds, and nobody could reproduce them, me included. Flow's own memory is in the absolute numbers above, and there is no Node-RED column next to it, because nobody has measured Node-RED the same way yet. A ratio across two methods is exactly the mistake that got the old numbers pulled.

Why not just keep running Node-RED

Four things, and none of them are fixable with configuration.

Node-RED is fine on a Pi in a workshop and it falls down on a plant floor, for four specific reasons. Flow reads your flows.json and runs it, and the migration guide walks a flow across. The one thing that doesn't come along is community nodes, because they are npm packages. Each card is the short version, and the receipts are under them.

There is no back-pressure.

Nothing puts a ceiling on Node-RED's wire queues, so one chatty sensor outruns a slow sink until the pod runs out of memory and gets killed, with nothing in the log. Every Flow inbox is bounded.

It is single-threaded.

One event loop runs the runtime, the editor and every node's I/O, so one CPU-heavy Function node freezes all of it. Flow gives every node a goroutine.

The Function node's sandbox is not a boundary.

Node-RED runs Function code in node:vm, and Node's own documentation says that is not a security mechanism. Flow runs it on goja with no host bindings.

Credentials are weakly protected.

Node-RED encrypts them with AES-256-CTR keyed by a raw SHA-256 of your secret, with no MAC and no key derivation. Flow uses AES-256-GCM under Argon2id.

The detail, with the sources

There is no back-pressure.

Node-RED, by default, puts a setImmediate between every wire hop, and nothing puts a ceiling on that queue. A fast source outruns a slow sink, the queue grows, and the pod runs out of memory and gets killed with nothing in the log to explain it. Every inbox in Flow is bounded, with a policy per node: block, drop the newest, drop the oldest, or raise it to a Catch node and let the flow decide.

It is single-threaded.

One event loop carries the runtime, the editor API, the websocket fan-out, and every node's I/O. One CPU-heavy Function node stalls all of it, including the editor you are using to find out why it stalled. In Flow, every node instance gets its own goroutine, ordered within a node and parallel across nodes.

The Function node's sandbox is not a boundary.

Node-RED's Function node runs your code in node:vm. That is not our accusation, it is Node's own documentation: “The node:vm module is not a security mechanism. Do not use it to run untrusted code.” Node-RED's own guide says an unsecured editor is only suitable on a trusted network, and anyone who can deploy a flow can run commands on the box, which is fine on a Pi in a workshop and not fine on a customer's plant floor. It is not hypothetical either: CVE-2025-41656 is unauthenticated remote command execution, rated critical, in Pilz's IndustrialPI 4 firmware, because authentication for the Node-RED server on it was not configured by default. That is a CVE in Pilz's product, not in Node-RED, and Node-RED's own guide says the editor is not secured by default.

Credentials are weakly protected.

They are encrypted with AES-256-CTR keyed by a raw SHA-256 of your secret. CTR has no MAC, so anyone who can write the credentials file on a shared volume can flip chosen plaintext bits, which turns “can write a file” into “can change a broker password to one they picked.” And plain SHA-256 is not a key derivation function. There is no CVE on either of those, and both are still real.

Compared with Node-RED, concretely

The same idea, rebuilt around what bites people in production.

AreaNode-REDFlow
RuntimeNode.js, one event loopGo, goroutine per node
Back-pressureNone, unbounded queueBounded inbox, four policies
Message cloningFirst recipient aliases the senderEvery recipient gets a copy
Function sandboxnode:vm, explicitly not a boundarygoja with no host bindings. Plugins are signed WebAssembly with a memory ceiling and a timeout
Pluginsnpm packages, in the same processSigned WebAssembly, refused unless the key is trusted, and no host access unless granted
exec nodeAny command, through a shell by defaultDisabled until allowlisted, and no shell at all
File nodesAny path the process can reachScoped to the volume, symlinks resolved
CredentialsAES-256-CTR, SHA-256 as the keyAES-256-GCM, Argon2id
Context APIget, setget, set, CompareAndSwap, Increment, Update
Flow file writesTemp file, fsync, renameTemp file, fsync, rename, fsync the directory, three backups deep
Configsettings.js, executable JavaScriptDeclarative YAML
AuthOff by defaultRefuses to start without it
MetricsNot comparedPrometheus at /metrics, per node
Node definitionA .js plus a hand-written .html twinOne Go descriptor

Flow is deliberately not identical. Node-RED hands the first recipient on a wire the original message and only clones for the rest, so two branches that each think they own their message can quietly corrupt each other, and which one breaks depends on the order you wired them. Every recipient in Flow gets its own copy. A small message costs 411 to 453 ns to copy. A 1 MiB binary payload would cost 128 to 156 µs per recipient, so those travel shared and immutable instead, at 250 to 279 ns, which is exactly where copying would have hurt. Every deliberate difference is written down in the compatibility matrix, so none of them is a surprise on a live line.

Compatibility

79 node types, and each one says how close it is to Node-RED.

A node that is partially compatible and silent about how is worse than one that is obviously absent, because the flow appears to work and quietly does the wrong thing. So a test fails the build if a node does not say what is missing.

12

Full

Behaves as the Node-RED node of the same type does.

21

Partial

A subset. The notes say exactly which parts are missing.

14

Divergent

Deliberately different, and the notes say why.

32

Flow only

No Node-RED counterpart.

Not supported at all: community nodes

They are npm packages that need Node.js. There is no version of this where they work.

JSONata, checked against the published examples

Every jsonata property runs on a pure Go JSONata 2.x that passes 1,673 of the 1,686 published test cases. The 13 misses are listed in the repository.

Browse all 79 nodes

Keep reading

Release notes, the roadmap and the migration guide.

Where it stands

It runs today. It has not run on a plant floor yet.

Verified against real infrastructure, not just its own encoders: MQTT, InfluxDB 2.7 and PostgreSQL 16 in Podman. The race detector is clean on every package. Here's what isn't done, roughly in the order it bugs me, so you know before you plan around it.

  • A physical PLC. Modbus TCP, OPC UA, S7, EtherNet/IP, and Sparkplug B nodes are built and were tried against simulators and a real broker. S7 and EtherNet/IP have only met simulators, and none of them has met a Siemens CPU or a Logix controller. Modbus RTU, Modbus over TLS, OPC UA events, history, method calls and browse, S7 timers and counters, and PCCC controllers are not done.
  • HotLoop nodes. Out in 2.3.0. Only the Modbus write was run through HotLoop’s real write gate. The OPC UA, S7 and EtherNet/IP write nodes send through the same code and a test checks each one, but none has been through a real gate. The live-feed reconnect is tested against a stand-in, and an https HotLoop with a private CA is not covered.
  • AI agents (MCP). Out in 2.3.0, tested against the Go SDK’s client, the MCP Inspector and a real HotLoop 4.18.10. Not done: OAuth, stdio servers, resources, prompts, sampling, and a tool’s output schema.
  • Plugins on a Pi. Signed WebAssembly plugins are out in 2.3.0 and have never run on a Pi. The traps and refusals are tested against guests built from Rust, TinyGo and Go, and the per-call numbers are from an x86 box.
  • A real Pi. The durable store’s writes per hour are measured on x86 under WSL. A Pi 4 and a Zero 2 W have not run it yet, and an SD card is the first place a context store would show a bug.
  • The editor. It was rebuilt in 2.5.0 in HotLoop's colors and type, and the 60 browser tests passed untouched. It has not been opened in Firefox or Safari, on a tablet, on a plant HMI screen, or inside the live EmberCORE dashboard frame. The screenshots and the browser tests run in Chrome.
  • It is new to the plant floor. A plant runs it through EmberNet now, and that is the only field proof it has. Everything past the deploy line is still proven against simulators and stand-ins, and it will not be described as production-hardened until a plant has tried to break it.
Licensing

One license, like the rest of HotLoop.

Flow is under the HotLoop Business Source License 1.1. Home and lab use is free, and business use is free through EmberNet. Each version converts to GPL-3.0 or later four years after its release. The Flow repository is public, so you can read all of it.

Read the license