Skip to content

Latest commit

Β 

History

49 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

node-red-cli πŸ”—

Checks License: MIT Node.js >=24

Install the CLI with npm install -g @tbrandenburg/node-red-cli.

Call Node-RED flows like Unix functions ⚑

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/null
9

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. 🧩

Install πŸ“¦

npm install -g @tbrandenburg/node-red-cli

This installs the node-red-cli command globally, ready to use against any Node-RED flow file (see Quick start below).

The idea πŸ’‘

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. πŸš«πŸ”§

Why node-red-cli? βœ…

  • 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 to stderr.
  • No flow mutation: the current implementation adds no temporary nodes and never redeploys flows.json.

Current state 🚧

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:

  1. βœ… A successful call returning { payload: 9 }.
  2. βœ… Preflight validation rejecting an unknown target.
  3. βœ… A timeout when a flow doesn't respond in time.
  4. βœ… An unchanged SHA-256 hash of the flow file before and after the call.

Project layout πŸ“

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

Quick start πŸš€

To build and run from a repo checkout instead (e.g. for contributing):

make install
make test

make 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-global

Try the CLI directly against the example flow:

echo '{"payload":{"x":4,"y":5}}' | node-red-cli test/fixtures/flows.json calculate
9

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.json

Instead 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/null
9

Passing flow JSON inline πŸ“₯

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/null
9

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.json
9

Installing additional Node-RED node packages πŸ“¦

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 the userDir persistent/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-dir entirely 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_modules before the flow runs. Repeatable and/or comma-separated. Requires an explicit --user-dir β€” using it with the default ephemeral userDir is 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/null

Already-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.

⚠️ Security note: --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".

⚠️ Persistent 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.

Host API πŸ› οΈ

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.

Technical approach πŸ”¬

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.

Preflight and limitations ⚠️

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 out with mode: "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.

Roadmap πŸ—ΊοΈ

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.

Status πŸ“Š

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.

Contributing 🀝

Contributions are welcome β€” see CONTRIBUTING.md.

Security πŸ”’

Please report vulnerabilities responsibly β€” see SECURITY.md.

License πŸ“„

MIT β€” see LICENSE.

About

Call existing Node-RED flows from Node.js and the command line

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages