Skip to content

Latest commit

Β 

History

26 Commits

Folders and files

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

Repository files navigation

node-red-cli πŸ”—

Checks License: MIT Node.js >=24

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

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 πŸš€

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 instead of via node bin/..., install it globally:

make install-global

Try the CLI directly against the example flow:

echo '{"payload":{"x":4,"y":5}}' | node bin/node-red-cli.js 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 bin/node-red-cli.js 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 bin/node-red-cli.js test/fixtures/flows.json calculate \
  --set x=4 --set y=5 < /dev/null
9

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