Source, flow, sink. Drawn once, run as one process.
You draw the route on a canvas. The same binary polls the sources, runs the nodes and delivers to the sinks. There is no second service to deploy.
- source
Every reading becomes one record.
Adapters emit
CanonicalMachineData: source, timestamp, machine, status, axes, spindle and free-form metadata. Quality travels with it, per record and per value: good, uncertain or bad. A source that goes silent produces an offline record, not a gap.modbus 103maps tospindle.speedcanonical record (telemetry.v1) { "source": "modbus", "timestamp": "2026-08-01T09:30:00Z", "machine": { "id": "PLC-01", "name": "Assembly Line PLC" }, "status": { "state": "running" }, "axes": [ { "name": "X", "position": 12.5, "unit": "mm" } ], "spindle": { "speed": 8000, "load": 41.5 }, "quality": "good", "metadata": { "temp": 61.2 } } - flow
Route, filter and reshape on a canvas.
Wire nodes together and each source can take its own path to its own sinks. Only the JavaScript transform starts a JavaScript engine; every other node is native Rust.
- source
- filter
- branch
- map
- transform
- http
- sink
- DSL for the common case. Filters and field math run against the in-memory record with no JSON round trip.
- JavaScript when you need loops or regex. Sandboxed: no filesystem, no network, an 8 MB heap and a 100 ms deadline by default.
- Edit with Node-RED's moves. Quick add on a dropped wire, link nodes across tabs, groups, wire splicing and Ctrl+F.
- Test before you deploy. Run a node against the last real record from its source and see each step, the diff and what would be delivered. Nothing is sent.
- Optional script drafting. Describe a rule in plain language and an OpenAI-compatible endpoint that you configure writes the script. The server validates it before it lands.


transform.dsl # keep only hot or vibrating machines when metadata.temp > 80 || metadata.vib > 5.0 set metadata.alert = true set metadata.temp_f = metadata.temp * 9 / 5 + 32transform.js function transform(record, ctx) { if (record.metadata.temp > 95) return null; // drop record.metadata.risk = record.metadata.temp * 0.8 + record.metadata.vib * 1.2; return record; } - sink
Fan out to every destination, concurrently.
Each route picks its sinks and a record goes to all of them at once, each under its own deadline (5 s by default). One slow sink does not hold up the others, and the poll loop never waits on the network.
- pushHMAC-signed POST to OpexMX or any receiver, through the durable queue.
- natsPublishes to a NATS subject, one subject per machine.
- mqttPublishes to an MQTT topic.
- storageSQLite history with retention by days and by size.
- clickhouseBatched writes to a ClickHouse table.
- postgresBatched writes to Postgres or a TimescaleDB hypertable.
- debug
Watch every record move.
From download to first record in minutes.
Run one binary, connect a PLC, a CNC or a webhook, and watch the records arrive in the editor.
- One binary
- Raspberry Pi to rack server
- Signed delivery with retries
Sizing questions or an air-gapped install? Email marketing@opexcg.com or read the quick start.

