Install the CLI with
npm install -g @tbrandenburg/node-red-cli.
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-red-cli 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. π§©
npm install -g @tbrandenburg/node-red-cliThis installs the node-red-cli command globally, ready to use against
any Node-RED flow file (see Quick start below).
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
To build and run from a repo checkout instead (e.g. for contributing):
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 from a repo checkout, install it
globally from the local source:
make install-globalTry the CLI directly against the example flow:
echo '{"payload":{"x":4,"y":5}}' | node-red-cli 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-red-cli 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-red-cli test/fixtures/flows.json calculate \
--set x=4 --set y=5 < /dev/null9
Instead of a <flows.json> file path, --flow-json <value> accepts the flow
definition directly, so in-memory callers (tests, another Node.js process, a
Node-RED editor "run this flow" action) never have to write a temp file just
to satisfy this CLI's file-based API. It is mutually exclusive with the
<flows.json> positional argument. <value> is one of:
- an inline JSON array:
--flow-json '[{"id":"a",...}]' -to read the flow JSON from stdin@<path>to read it from a file (equivalent to the positional argument)
The flow is never written to disk in any of these forms.
node-red-cli --flow-json @test/fixtures/flows.json calculate \
--set x=4 --set y=5 < /dev/null9
Since stdin is also used to read the msg payload, --flow-json - and the
stdin msg are mutually exclusive: when --flow-json - is used, stdin is
consumed by the flow definition instead, so msg must be built entirely from
--set params:
node-red-cli --flow-json - calculate --set x=4 --set y=5 \
< test/fixtures/flows.json9
By default the CLI creates a fresh, ephemeral Node-RED userDir per
invocation and deletes it afterwards, so only the node types bundled with
node-red itself are available to a flow. To use community/custom nodes
(e.g. node-red-contrib-something), two options work together:
--user-dir [path]makes theuserDirpersistent/reusable across runs instead of ephemeral. Pass a path to use a specific directory, or the bare flag to use a stable cache dir ($XDG_CACHE_HOME/node-red-cli, falling back to~/.cache/node-red-cli). Omitting--user-direntirely preserves today's ephemeral behavior unchanged.--node-modules <name[@version]>[,...]installs any of the given Node-RED node npm packages that are missing from<userDir>/node_modulesbefore the flow runs. Repeatable and/or comma-separated. Requires an explicit--user-dirβ using it with the default ephemeraluserDiris rejected with a clear error, since the installed module would be thrown away immediately and reinstalled from npm on every single invocation.
node-red-cli flows.json calculate \
--user-dir ~/.cache/node-red-cli \
--node-modules node-red-node-random \
--set x=4 --set y=5 < /dev/nullAlready-installed, version-matching modules are left untouched, so repeat
runs against a warm cache do not touch the network. No invocation ever
reaches out to npm unless --node-modules is explicitly passed.
--node-modules runs a real npm install, i.e.
arbitrary code execution from whatever npm registry is configured. Only
use it with trusted module names. A minimal built-in denylist blocks
obviously unsafe values (path traversal, URLs, whitespace); operators can
add exact names or *-glob patterns via the NODE_RED_CLI_DENY_MODULES
environment variable (comma-separated), e.g.
NODE_RED_CLI_DENY_MODULES="node-red-contrib-*-internal".
userDir caveat: a shared userDir accumulates
Node-RED runtime/state files (e.g. .config.runtime.json) across runs.
Delete the directory (or the default ~/.cache/node-red-cli) to clear the
cache and start fresh.
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.