OpexFlow is built around four primitives. Everything else is plumbing.
| Primitive | Role |
|---|---|
| Source (adapter) | Ingests events from a protocol: Modbus poll, OPC-UA polled read, HTTP REST poll, MQTT topic, NATS subject. |
| Transform (node) | Reshapes each event before fan-out: a DSL program or a JavaScript function; alongside filter, branch, map and http nodes. |
| Sink (node) | Delivers the result to one target: hub (WebSocket), HMAC-signed HTTP push, NATS, MQTT, SQLite, ClickHouse, Postgres. |
| Flow (document) | The single wiring of sources → nodes → sinks, edited on the canvas and stored in <data_dir>/flow.json. |
Every event is normalized to CanonicalMachineData at the source boundary, so
transforms and sinks always see the same shape regardless of protocol.
One flow document
There is one routing model, the flow — there is no separate “transform route” vs
“pipeline”. A canvas source node carries a match key:
match |
Picks |
|---|---|
modbus:PLC-01 |
records of one machine ({source}:{machine.id}) |
modbus:* |
every record of one adapter |
* |
everything not picked by a more specific source |
Per record the engine looks up the exact key, then {source}:*, then *; match is unique
across source nodes. A record no source matches is counted as unrouted (visible in the live
stats) and dropped. The pipeline of a source is the subgraph reachable from it; a node may be
shared by several sources, and a source with several wires fans out. Deploying validates and
compiles every source first and swaps them all at once, so a bad edit never leaves the running
flow half-changed.
The canvas can hold several named flows (tabs, flows: [{ id, name, disabled? }] in the
document; each node carries its flow id). They only group nodes: routing is still global, so a
wire never crosses flows, a match is unique across all of them, and disabling a flow switches off
all its nodes at once.
On first start (no flow.json yet) the flow is seeded from the config’s transform
section and the legacy pipelines.runtime.json / flows.layout.json with the same routing as
before (a route without its own sinks inherits the default’s). From the first deploy on,
flow.json is authoritative and the config’s transform routes are ignored.