Chapter 17

Links, Subflows, environment variables, the Library, and import/export

When a flow grows from a single automation into multiple tabs, reuse is no longer just a matter of drawing fewer wires. You must also define clear boundaries for calls, state, configuration references, and portable data. Using the Node-RED 5.0.2 bundled with Add-on 22.0.1 as its baseline, this chapter guides you through designing a traceable architecture that can be shared safely, using only offline, manual examples.

Define the boundaries of reuse first

Once the same logic has been copied three times, a fix will usually reach only two copies. As long wires multiply across tabs, it also becomes harder to tell where data originated. Node-RED provides Links, Subflows, the Library, import/export, and Projects, but each solves a different problem. Treating them as a single kind of “reuse” obscures call-waiting behavior, per-instance state, configuration-node references, and responsibility for version control.

ToolBest suited toWhat it does not guarantee
Link In/Link OutOne-way virtual wiring across tabsIt is not a function call and does not return automatically
Link Call + returnA time-bounded request/response exchangeA timeout neither cancels work nor suppresses a late return
SubflowReuse one internal graph through multiple instancesIt does not share a single node context, and cannot recursively contain itself
LibrarySave flow or Function content for reuse in the editorIt is neither deployment history nor a remote Git workflow
Import/exportMove a snapshot of flow JSONIt is not evidence of trust and does not automatically remove environment-specific identifiers
ProjectsGit-backed projects, branches, and version-control workIt is disabled by default in this Add-on; the standard workflow tracks an encrypted project credentials file, but still does not provide a complete backup

The examples in this chapter manipulate only strings and numbers within messages. They do not use the network, files, Home Assistant actions, or any other external I/O. A Comment node can document an interface on the canvas; a Junction is only a wire-routing intersection in the editor and cannot serve as a Link, call, or reusable component. If msg and Context are not yet familiar, first revisit Chapter 5, the message model, and Chapter 7, Context.

Interface, state, and configuration references are three separate axes

Every reusable component should document at least three things: its input/output contract, state ownership, and external configuration dependencies. The input contract might define the accepted msg.payload type and error behavior. State ownership determines whether two instances affect each other. Configuration dependencies identify whether nodes in the flow refer to a Home Assistant Server, an MQTT broker, or another configuration node.

Contract surfaceOffline exampleValidation question
Inputmsg.payload must be a finite numberShould strings, null, and missing values be rejected or converted?
OutputOn success, output the same msg and add only msg.resultDoes it overwrite any field that the caller needs?
ErrorReport a catchable error without putting the complete input in the error stringCan the caller distinguish a validation failure from a timeout?
StateStore the count in node context within the instanceShould two instances maintain separate counts?
ConfigurationSupply a non-secret label through an instance propertyIs there a safe default when the property is missing?

A configuration node is not an ordinary message-processing node. Other nodes refer to it by ID in JSON. When selected nodes are exported, the editor may include their required configuration nodes or leave references that cannot be resolved in the target environment. Do not edit JSON IDs by hand to make them “match” production, and do not commit real server names, broker details, URLs, or credential values to a file intended for sharing. Instead, import into an isolated copy, open each dependent node, and use the editor to select an approved configuration node from the target environment.

Build one offline Link Call from end to end

  1. Create a manual request entry point.

    On an isolated tab, add an Inject node named “Manual call LC-001.” Set the payload type to number and its value to 4. Under properties, also add the string msg.requestId = "LC-001"; set once=false, and leave repeat and crontab blank. Connect it to a Change node named “Build request contract.” Use JSONata to set msg.request to {"requestId": requestId, "value": payload}, then delete msg.payload.

  2. Configure a static Link Call.

    Add a Link Call named “Call / offline doubling.” Choose the static call type, set the timeout to 2 seconds, and explicitly select the Link In named in the next step as the target; do not use msg.target. The wiring must be “Manual call LC-001” → “Build request contract” → “Call / offline doubling.”

  3. Create a named callee path.

    On another regular flow tab, add a Link In named “Callee / offline doubling.” Connect it to a pure Function named “Validate and double,” then to a Link Out named “Return / offline doubling” with its mode set to return. The Function must use only the following code, read no context, and perform no external I/O:

    const request = msg.request;
    if (!request || typeof request.requestId !== "string"
        || typeof request.value !== "number" || !Number.isFinite(request.value)) {
        node.error("INVALID_REQUEST", msg);
        return null;
    }
    msg.response = {
        requestId: request.requestId,
        doubled: request.value * 2
    };
    return msg;
  4. Observe normal responses and timeouts separately.

    Connect the Link Call output to a Debug node named “Inspect response” that displays only msg.response. Add a Catch node named “Catch Link Call timeout,” scope it only to “Call / offline doubling,” and connect it to a Debug node that displays only msg.error. In the normal test, press Inject once. The sole normal projection is {"requestId":"LC-001","doubled":8}, and Catch receives nothing. To test the timeout, temporarily disconnect only the wire from “Validate and double” to the return Link Out. After 2 seconds, Catch reports msg.error.message as timeout; the source type is link call, the source name is “Call / offline doubling,” and the normal Debug receives nothing. Reconnect the return wire immediately after the test.

  5. Verify, clean up, and export.

    Confirm that the complete path is exactly manual Inject → Change/request contract → static Link Call → named Link In → pure transform → return Link Out → limited Debug. It must contain no Action, HTTP, MQTT, file, scheduling, or credentials. Clear the Debug messages before making the smallest possible export and reviewing it as text. For deployment and recovery strategies, continue with Chapter 18, Debugging and testing.

Input: msg.payload = 4; msg.requestId = "LC-001"
request: { requestId: "LC-001", value: 4 }
Normal Debug: { requestId: "LC-001", doubled: 8 }
timeout Catch: error.message = "timeout"; normal Debug receives no message
External I/O: none; Link Call timeout: 2 seconds

Subflow instances, properties, and Context

A Subflow is a reusable internal graph of nodes. Each time you drag it onto a flow, you create an instance. Changes to its definition affect its instances, so a Subflow suits “the same algorithm with different non-secret parameters,” not copies intended to diverge indefinitely. The nodes within each instance are distinct runtime instances, and their node context should likewise be treated as isolated. Deliberately reading or writing the parent flow context crosses that isolation boundary.

The Subflow path is a design checklist, not part of this chapter’s hands-on exercise: Before choosing a Subflow, document its input/output shape, the non-secret default for each instance property, ownership of node/parent/global context, expected A→B→A interleaving across two instances, how errors leave the Subflow, which instances a definition update will affect, and the check that prevents recursion. Once every item has an answer, validate it in a separate isolated example; do not combine it with the Link Call walkthrough above.

Subflow properties become environment values for a given instance, allowing non-sensitive parameters such as DISPLAY_LABEL and MAX_COUNT. A Function can retrieve them with env.get("DISPLAY_LABEL"), and a typed input can select the environment-variable type. The Node-RED runtime also provides instance information such as NR_SUBFLOW_ID, NR_SUBFLOW_NAME, and NR_SUBFLOW_PATH. These values help distinguish instances during diagnostics, but IDs and paths must not appear in public examples or shared records.

// Pure-data Function inside a Subflow
const label = env.get("DISPLAY_LABEL") || "Unnamed instance";
const current = context.get("seen") || 0;
context.set("seen", current + 1);
msg.result = { label, sequence: current + 1 };
return msg;

With two instances of this example, each instance’s seen count should start at 1. If the requirement is genuinely for shared statistics, first document the shared state’s lifecycle, then deliberately choose flow or global context; do not use it simply because it is accessible. A Subflow can access its parent context/environment through $parent., but this increases coupling and must be documented as part of the interface. Even when a Context store uses localfilesystem, an assignment does not necessarily become durable immediately. Context is not a secret store either.

DataRecommended locationReason
Temporary count for one internal nodeNode contextNaturally isolated by instance
An instance’s display label or limitSubflow propertyEach instance can set it explicitly
Non-secret state shared by several nodes in the parent flowExplicit parent flow contextCross-boundary sharing must be documented in the contract
Token, password, or private keyNeither a property nor ContextUse managed credentials or the platform’s secret mechanism

Do not use recursion: Never place a Subflow instance directly inside its own definition, or create a cycle in which A contains B and B contains A. This is not ordinary function recursion; it creates an architecture that cannot be expanded safely and poses a resource risk. For repeated processing, use bounded message iteration or a finite-state design instead, and test its termination condition.

Boundaries between environment variables, the Library, and Projects

In this chapter, environment variables are used only for non-sensitive, replaceable configuration, such as display text or a batch limit. Do not put tokens, passwords, private keys, or connection credentials in flow JSON, Subflow properties, Function code, Debug output, or Context. Environment-variable names and values may also be exposed through exports, diagnostics, or the runtime environment. Merely “using env” does not make a secret secure.

The Library is a reusable store within the editor: you can save flow snippets or Function code and retrieve them later. Import/export serializes the current selection to JSON or brings JSON into the editor, making it suitable for an explicit, one-time transfer. Projects provides a complete Git-backed working directory and version-control workflow involving a repository, branches, remotes, and Git identity. None of these three replaces a managed backup, and none performs secret scrubbing for you.

AspectLibraryImport/exportProjects
UnitReusable snippet or FunctionOne JSON snapshotFiles in a Git-backed project
HistoryMust not be treated as complete change historyYou store and compare snapshots separatelyGit commits and branches manage history
DependenciesNodes and configuration still require review after retrievalMissing node types and configuration still require review on importPackages and runtime differences still require management
SecretsScrub before savingScrub before sharingThe standard workflow commits the encrypted project credentials file; the repository must remain private, and the project credential secret must be stored separately
Add-on boundary: In Add-on 22.0.1, editorTheme.projects.enabled defaults to false. Do not follow an upstream Projects tutorial on the assumption that the interface is already available, and do not directly edit a settings file managed by the Add-on. If your organization approves enabling Projects, first create a recoverable backup, record the current state, stop the Add-on, make the change through a method supported by the Add-on, then restart and verify. Follow the pinned Add-on documentation and your change-control process for the exact procedure.

Once Projects is enabled, access to Git remotes and Git credentials forms another security boundary. The standard Projects workflow in Node-RED 5.0.2 adds the encrypted project credentials file to version control. This is expected behavior, but does not make the file safe to publish: access to both the project repository and its history must remain restricted. Each project has its own project credential secret. Back it up securely and separately from the repository, or the encrypted credentials may be unrecoverable.

The Add-on’s credential_secret is ignored in Projects mode and cannot decrypt project credentials. The project repository, project credential secret, Git access credentials, and a complete Add-on backup each serve a different purpose. Git history is not a complete backup of the runtime environment, and an Add-on backup does not replace auditable version history.

Safe exports, configuration nodes, and rebinding

Flow JSON is readable and comparable, which also makes it easy to carry environment details elsewhere. Begin an export with the smallest possible selection; do not select an entire workspace merely for convenience. If a snippet depends on a configuration node, document the type of configuration it requires rather than retaining a real production ID. Credentials are generally stored through a separate mechanism, but that does not guarantee a clean export. Ordinary fields, Functions, Templates, Comments, environment properties, and Debug output may all contain sensitive values.

Pre-sharing scrub checklist

  • No credentials block, token, password, private key, cookie, Authorization value, or webhook identifier.
  • No real URL, hostname, IP address, Home Assistant server/entity/device/area ID, MQTT client ID, or topic.
  • Every configuration-node reference has been identified; in the target environment, use the editor to reselect an approved configuration node rather than replacing a JSON ID directly.
  • No Inject set to run at startup, schedule, listener, external connection, file write, or action path; if the snippet retains an operational node, disable it before export and document why.
  • Every Function, Template, Change, JSONata expression, Comment, node name, and flow description has been reviewed character by character.
  • Only required wires remain; Link targets, Subflow definitions, and instance properties are complete and contain no recursion.
  • Inspect the JSON as plain text, then import it into a blank, isolated tab; verify the node count, types, disabled state, and configuration dependencies.
Shared package notes (no real identifying data)
- Requires: Node-RED 5.0.2 core nodes
- Input: finite numeric msg.payload
- Config nodes: none
- Environment property: DISPLAY_LABEL (non-secret text)
- External I/O: none
- Runs automatically at startup: no
- Tested: normal value, missing value, wrong type, and isolation between two instances

Before importing unknown JSON, inspect type, wires, d, the Inject node’s once setting, and all configuration fields in a text viewer. If a node type is missing, do not immediately install a package just to clear the warning; first verify the source, pinned version, and necessity. This section’s checklist is self-contained. For complete procedures covering credentials, backup and restore, Projects, and security hardening, continue with Chapter 21, Backups and security.

Troubleshooting

  • Link Call waits until it reports a timeout: Confirm that the target is a Link In, that every success and rejection path ends at a Link Out in return mode, and that a regular send-to-all Link Out has not been mistaken for return. Scope a Catch node to the call to observe its errors; do not merely lengthen the timeout to hide a missing path.
  • The return node warns that no return source exists: The path may have been entered through a regular Link or Inject rather than a Link Call. Keep one-way and call entry points separate so they do not share a return that is valid only within a call chain.
  • Values in two Subflow instances affect each other: Check whether the implementation uses flow/global or $parent. context instead of node context within the instance. Then send manual inputs in A→B→A order and verify each count independently.
  • An unknown configuration node or red triangle appears after import: Do not edit the reference ID by hand. Open the node that uses the configuration node, confirm its type and fields, then reselect an approved configuration in the target editor. If no approved configuration is available, leave the node disabled.
  • Projects is not available: Projects is disabled by default in this Add-on; this is not a browser problem. Do not edit settings directly. Follow the approved process for backup, shutdown, controlled modification, restart, and recovery.
  • An export appears to contain no credentials but still cannot be shared: Search ordinary fields for environment IDs, URLs, comments, Function strings, Subflow properties, and configuration nodes. Have another person review the plain text; do not rely solely on the export dialog’s scope summary.

Pinned versions and official documentation

The exact commits below define this chapter’s behavioral baseline. The official documentation supplements the operating concepts. If rolling documentation differs from the pinned versions, follow the implementations in the pinned Node-RED 5.0.2 and Add-on 22.0.1 commits.

Frequently asked questions

What should I do first if a Projects repository accidentally becomes public?
First stop synchronization and automatic pushes, move the remote back to a controlled private location, and revoke the associated Git access credentials. Then treat the project credential secret and any tokens or passwords that may appear in flows, configuration, or history as compromised, and rotate them. Deleting the current files does not remove them from Git history. Rewrite or quarantine the contaminated history according to your organization’s procedures, then verify the environment again from a trusted backup.
Can I reuse a requestId immediately after a timeout?
No. An earlier call may return later, and reusing its ID prevents the gate from distinguishing the two generations. Use a new, opaque requestId for every call. Remove the pending record on timeout, and discard any late return; it must not reactivate the old work.
Which regression is easiest to miss after changing a Subflow definition?
Testing only one instance. Create at least two instances with different properties and interleave input in A→B→A order. Confirm that node context does not leak between them, that sharing through parent flow/global context matches the design, and that every tab and error exit using the Subflow still behaves correctly.
Should environment variables hold tokens?
Not in this chapter. Subflow properties, environment values, Context, Debug output, and exports may all expose data. Put secrets in managed credentials or a secret mechanism provided by the platform, and still scrub every field before sharing.
Does enabling Projects give me a backup?
No. The standard Projects workflow tracks an encrypted project credentials file, but the repository must not be public and the project credential secret must be backed up separately and securely. The Add-on’s credential_secret is ignored in Projects mode. A complete Add-on backup and Git version history continue to serve separate purposes.
Can I edit a JSON ID directly when a configuration-node reference breaks after import?
No. Keep the affected nodes disabled, then use the editor to select approved configuration nodes of the correct type in the target environment. Guessing or pasting a real ID can bind the wrong configuration and leak environment data into the shared file.