node-red-cli explores a simple, powerful idea: existing Node-RED flows
should be usable from a CLI or a Node.js host just like ordinary functions.
node-red-cli flows.json calculate --set x=4 --set y=5 < /dev/null9
By default only the resulting payload is printed as plain text. Pass
--format=json to print the full result object as JSON instead:
node bin/node-red-cli.js test/fixtures/flows.json calculate \
--set x=4 --set y=5 --format=json < /dev/null{ "payload": 9, "_msgid": "..." }This turns Node-RED from a visual automation tool into a reusable runtime building block for scripts, services, pipelines, and developer tooling. π§©
An existing flow becomes a clean input/output interface:
stdin / CLI args
|
v
Node-RED runtime
|
v
link in: calculate -> any flow -> link out: return
|
v
stdout / Promise<Result>
The flow itself stays untouched. No extra CLI nodes, no copy-pasted logic, and no permanently deployed adapter structure. π«π§
- Reuse existing flows: business logic stays where it's already maintained β in Node-RED.
- Uses the real Node-RED runtime: core and contrib nodes don't need to be reimplemented.
- CLI-friendly I/O: JSON in, JSON out.
- Async support included: Node-RED flows keep working exactly as they normally do.
- Safely bounded calls: timeouts prevent a process from hanging forever.
- Clean separation: results go to
stdout, logs and errors go tostderr. - No flow mutation: the current implementation adds no temporary nodes and
never redeploys
flows.json.
This repository provides an early-stage CLI and host-side adapter for
Node-RED 5.0.4. The adapter invokes an existing link in node and captures
the response from a link out node in return mode as a Promise.
The included example flow (test/fixtures/flows.json) computes x + y:
link in: calculate -> Function -> link out: return
The test suite (test/e2e/flow.e2e.test.js) verifies:
- β
A successful call returning
{ payload: 9 }. - β Preflight validation rejecting an unknown target.
- β A timeout when a flow doesn't respond in time.
- β An unchanged SHA-256 hash of the flow file before and after the call.
bin/ CLI entrypoint (node-red-cli)
src/ Host-side link-call adapter (library API)
test/unit/ Fast tests against a fake Node-RED runtime
test/integration/ Adapter tests against a real embedded runtime
test/e2e/ Full round trip through the example flow
test/fixtures/ Example Node-RED flow used as a test asset
make install
make testmake install also wires up a pre-push git hook that runs make ci
(format, lint, and tests) automatically before every push.
To use node-red-cli as a regular command instead of via node bin/...,
install it globally:
make install-globalTry the CLI directly against the example flow:
echo '{"payload":{"x":4,"y":5}}' | node bin/node-red-cli.js test/fixtures/flows.json calculate9
The _msgid is generated by Node-RED and differs on every run. To see it
along with the rest of the result object, pass --format=json.
The target argument is optional; if the flow has exactly one link in
node, it is used automatically (with a warning on stderr if it also had to be
inferred across multiple tabs):
echo '{"payload":{"x":4,"y":5}}' | node bin/node-red-cli.js test/fixtures/single-link-in.flows.jsonInstead of building the whole JSON message yourself, individual payload
attributes can be set directly from CLI params with repeatable
--set <key>=<value> flags. Values are JSON-parsed when possible (so 4
becomes a number, true a boolean), otherwise kept as plain strings, and they
are applied on top of (and override) any payload read from stdin:
node bin/node-red-cli.js test/fixtures/flows.json calculate \
--set x=4 --set y=5 < /dev/null9
The core interface is intentionally small:
const { createHostLinkCaller } = require("./src/link-call");
const caller = createHostLinkCaller(RED);
const result = await caller.call(
"calculate",
{ payload: { x: 4, y: 5 } },
{ flow: "calculator", timeout: 5000 }
);
console.log(result.payload); // 9
caller.close();flow accepts either the tab ID or the unique tab label. If omitted, the only
existing workspace tab is selected automatically.
target (the link in node) is also optional. If omitted, the only link in
node in the resolved flow is used automatically. If no flow is given and
several tabs exist, but only one link in node is present overall, that node
(and its tab) is inferred and a warning is reported via the optional
onWarning callback β pass one to caller.call(...) to observe it:
const result = await caller.call(
undefined,
{ payload: { x: 4, y: 5 } },
{
onWarning: (warning) => console.error(warning)
}
);If either the flow or the target remains ambiguous (more than one candidate),
call() rejects with a preflight validation error naming what must be
specified explicitly.
Node-RED's link-call semantics use _linkSource to make the origin of a call
available to a return link. This adapter sets the required stack entry on the
host side and registers a targeted onReceive hook. The returned message
resolves the Promise before the link-out node needs to resolve the caller via
RED.nodes.getNode(...).
This is a lightweight compatibility layer for Node-RED 5.0.x, not a public
runtime API. The internal semantics are therefore encapsulated behind
createHostLinkCaller(RED) and should be integration-tested separately for
each supported Node-RED version.
Before a call, validateTarget(RED, targetId) checks:
- target ID and target type
link in - instantiation of the target node
- missing wire targets and duplicate IDs
- at least one reachable
link outwithmode: "return" - instantiation of reachable return nodes
- availability of the required runtime hooks
Validation does not prove that a flow terminates semantically or replies exactly once. A runtime timeout remains necessary for that.
The long-term goal is a stable, official host API in Node-RED core:
const result = await callNodeRedFlow({
target: "calculate",
msg,
timeout: 5000
});Or as a runtime interface:
const result = await RED.runtime.flows.call("calculate", msg, {
timeout: 5000
});The research focuses on which parts of the existing node.linkcall()
implementation can be generalized, and what a small upstream API such as
RED.nodes.callLink() or RED.runtime.flows.call() could look like.
Early-stage CLI: the approach works for the included Node-RED 5.0.4 example flow. The link-call internals used here are not stabilized as a public Node-RED API. Production use therefore requires deliberate version pinning, integration tests, and robust error handling for ambiguous or non-terminating flows.
Contributions are welcome β see CONTRIBUTING.md.
Please report vulnerabilities responsibly β see SECURITY.md.
MIT β see LICENSE.