Chapter 6

Flow Logic with Change, Switch, Range, Join, and Split

First, use Change to create a consistent message structure. Then use Switch and Range to build routing that is easy to understand. Introduce Split, Join, Sort, and Batch only when the data volume is bounded and item-by-item processing is genuinely necessary. This chapter uses manual inputs and Debug nodes throughout; it does not trigger any real household devices.

Why This Chapter Matters

A reliable home automation flow must handle more than an ordinary temperature reading such as 23. It must also handle missing values, strings, out-of-range numbers, unknown topics, and sequences with missing parts explicitly. Cramming all the logic into one Function node may work, but the canvas will not clearly show where data is rewritten or which condition leads to which output. Core logic nodes make these decisions visible in the nodes and wiring.

In Node-RED 5.0.2, the core function nodes include Switch, Change, and Range, as well as Template, Delay, Trigger, Exec, RBE, and Function. This chapter focuses on the first three. Delay and Trigger are covered in Chapter 7, while Template and Function are covered in Chapter 15 and Chapter 16, respectively. When you need to process arrays or batches, continue to the later sections on Split/Join, Sort, and Batch.

Design order: Validate and normalize → route → scale if necessary → process a bounded sequence → join → verify with Debug. Never allow failed data to fall through silently to an output that controls a device.

Core Concepts

  • Change maps data: It can set, change, delete, or move a msg, flow, or global property. Typed values determine each value's source and type. Rules run in the order shown in the editor, so a later rule can read the result of an earlier one.
  • Switch routes messages: It reads a specified property and sends the message to one or more outputs according to its rules. Whether it checks every matching rule is an important part of its behavior. Always provide an else route or document an explicit reason for discarding unmatched messages.
  • Range transforms numbers: It maps an input range linearly to an output range and can scale, clamp, roll, or drop values outside that range. It is not a data validator; intercept non-numeric inputs first.
  • A sequence defines a relationship among multiple messages: Split produces multiple messages with msg.parts. Join, Sort, and Batch retain messages and later emit results according to this metadata or manually configured conditions.
  • Waiting consumes resources: Every message retained by Join, Sort, or Batch occupies memory. If a source may be unbounded or may never send an ending message, limit its size, define a termination condition, and make failures observable.

A sound design distinguishes the possible reasons for “no output.” An unmatched condition, a Range drop, an incomplete sequence, an error, and a disabled node can all look the same. Add a temporary Debug node at each stage, and use verb-led node names such as “Reject non-numbers,” “Route comfort range,” and “Wait for complete batch.”

Import and Verify the Safe Routing Example

  1. Read the flow JSON first.

    Open 02-switch-routing.json. Confirm that it contains only a tab and Inject, Switch, and Debug nodes—no credentials, Home Assistant server, network endpoint, or node with side effects.

  2. Import it into a new test tab.

    Use the editor's Import feature to paste in the file contents. Inspect all five nodes again after importing. Never deploy third-party JSON that you do not understand.

  3. Test the matching route manually.

    The example Inject node sends the string “開啟” (“turn on”). The Switch node reads msg.payload; its first rule performs a string comparison, and its second rule is else. After you trigger the Inject node, only the corresponding Debug node should display the value.

  4. Create counterexamples.

    Duplicate the Inject node and test “關閉” (“turn off”), an empty string, and a number separately. Confirm that each follows the other route rather than treating values that look similar in the editor as the same type.

  5. Then add normalization with Change.

    Insert a Change node before Switch, move the test input to msg.command, and set a fixed msg.topic. Check the typed value for every rule individually, deploy, and rerun every test case.

Disable any Debug nodes you no longer need when testing is complete. This example demonstrates routing only: “開啟” is text and does not call a Home Assistant Action. To control a device, you must still add an allowlist, state preconditions, and an explicit target as described in later chapters.

Change: Create a Maintainable Data Boundary

Change is well suited to moving fields, applying default values, and normalizing data structures. The 5.0.2 editor provides four operations: set, change, delete, and move. The set and change operations accept typed inputs, and set also offers a deep-copy option. The following plan handles input from an indoor sensor without requiring a real entity ID:

OperationSourceDestination
setmsg.payload.temperaturemsg.measurement.value
setstring °Cmsg.measurement.unit
setmsg.topicmsg.measurement.source
deletemsg.authorizationRemove a sensitive field that should not be present before the message enters a general-purpose diagnostics branch

Rule order changes the result. If you move payload before reading payload.temperature, the later rule can no longer find the original path. After editing, compare the complete message in a Debug node. If you copy an object to another property and both copies may later be modified, enable deep copy and verify that changes to nested properties remain independent.

Change can also write to flow and global context, but the ability to do so does not mean that it should. Keep temporary derived values within msg whenever possible. Consider context only for state that must span messages, and define its scope, limits, and restart semantics as described in Chapter 7. Never put credentials, complete events, or unbounded arrays in context.

A name that describes the input and output is more useful than “change 1.” For example, “Map measurement fields” tells a reviewer what the node does at a glance. Use the node description to record accepted types and the route for missing values. If the list of rules becomes difficult to read, divide it into two nodes, such as “Map validated input” and “Clean public output.”

Switch and Range: Conditions and Numeric Policies

A Switch property can use the types offered by the editor, including msg, flow, global, and expression values. Start with a rule that handles a missing property or type mismatch, then define the business ranges, and finish with else. Temperature test values might be routed as below the lower limit, within the comfort range, above the upper limit, or other. Connect each output to a Debug node for verification—not directly to a heating or air-conditioning Action node.

“check all rules” and “stop after first match” determine whether one message can be sent through multiple outputs. Overlapping conditions such as “greater than 20” and “greater than 25” may both match. If an output will eventually cause a side effect, first design mutually exclusive ranges, then use a test matrix to prove that each input follows only the intended route. Never infer execution order from the visual positions of output wires.

Test inputExpected routeMistake to avoid
number 23Normal rangeConfusing it with string “23”
Missing propertyMissing-value branchTreating it as normal after it falls through to else
nullNo-value branchTreating it as 0
Extreme numberOut-of-range branchPassing it to Range without validation

In the Node-RED 5.0.2 source, the Range action options are scale, clamp, roll, and drop. Suppose a brightness percentage of 0–100 is mapped to an internal range of 0–1. Scale applies the linear mapping beyond the configured range; clamp limits out-of-range results to the nearest output endpoint; roll wraps out-of-range values around; and drop emits no message for an out-of-range value. Each option represents a product policy; none is universally correct. In home control flows, it is generally safer to reject anomalous source data before Range than to use clamp to conceal a sensor error.

Advanced: Split and Join

If you currently need only conditional routing, you can skip ahead to Troubleshooting and return here when you need to process arrays or batches.

Show Split/Join sequence details

Split can divide a string, array, object, or Buffer. It creates msg.parts to describe the sequence. For an array, the source sets a sequence id, index, count, and len. An object also has a key; a string or Buffer carries type and delimiter-related information. If the input already has parts, Split places the old parts in a nested stack so that Join can reconstruct a multilevel sequence.

Consider this scenario: inject a fictional array containing no more than five room readings, split it into individual messages, use Switch to remove invalid values, map fields with Change, and finally join the messages back into an array. There is a critical failure mode: if Switch discards one part, an automatic Join may continue waiting for a nonexistent message because the original parts.count still describes the complete sequence. The core Switch node has sequence-specific rules and pending-group behavior, but you must still define a policy for missing parts. Do not assume that arbitrary filtering will always repair the metadata automatically.

Join can combine messages in automatic, manual, or reduce mode; use the options shown in the 5.0.2 editor. Automatic mode reconstructs data from the metadata created by Split. Manual mode requires an explicit count, timeout, or completion signal. A stream with no end marker is unsuitable for unbounded waiting. Even with a timeout, you must decide whether to discard partial data, mark it as incomplete, or send it to an isolation branch.

Sequence safety: Limit the maximum number of input elements before using Split. Never split an arbitrarily large array from an untrusted API, and never allow Join to retain messages indefinitely while waiting for a count that will never arrive. If necessary, use the runtime's nodeMaxMessageBufferLength as a final safeguard, but keep the flow itself bounded.

At minimum, test an empty array, one element, an ordinary multi-element array, a sequence that loses one part, two interleaved sequences, and a duplicate index. Inspect msg.parts.id/index/count in Debug, and remove all environment-specific data before sharing the output.

Advanced: Sort, Batch, and msg.parts

Show Sort, Batch, and msg.parts details

Sort can order an array within one message, or collect and order a sequence using msg.parts. In sequence mode, it verifies that parts contains at least an id and index, then rewrites the indexes after sorting. Specify the sort key's type and direction explicitly: sorting numbers as strings produces different results. If equal keys require a stable order, verify that behavior with test data rather than relying on incidental arrival order.

Batch 5.0.2 supports modes for count-based overlapping batches, time intervals, and concatenating sequences by topic. It creates or updates msg.parts, and some modes require complete id, index, and count values. Overlapping windows cause the same message to appear in more than one batch; that is the configured behavior, not a duplicate-delivery bug. Whether a timed batch emits an empty sequence when no data arrives depends on its options. Do not guess from the option name—test it with Inject before deployment.

msg.parts fieldRoleCommon failure
idDistinguishes concurrent sequencesOverwriting it manually mixes two batches
indexIdentifies the part's position, usually starting at 0Filtering leaves gaps, duplicates, or an incorrect order
countRecords the known total number of partsThe declared number of parts never arrives
type/key/ch/lenDescribes the original container and reconstruction detailsChange deletes it accidentally, so Join reconstructs the wrong structure
partsStores the nested sequence stackThe hierarchy is misunderstood after multiple Split operations

In a home scenario, Batch can collect test-only notifications from a short interval into small summaries. Do not use it to delay a safety alert or allow its window to grow without bound. After sorting or batching, use Debug to verify count, index, and payload before passing messages to any external notification node. Do not assume that pending sequences held in memory will survive a flow restart; allow the source to resend them or discard incomplete batches safely.

Troubleshooting

  • Switch sends one message to two outputs: Check whether the rules overlap and whether the node is configured to check all rules. Make the conditions mutually exclusive and rerun the boundary-value tests. Do not rely on wire order to prevent side effects.
  • No message appears after Range: Confirm that the property exists and is a number, then check whether the action is drop. Compare Debug output before and after Range; do not switch directly to clamp merely to hide a source-data error.
  • Join waits forever: Inspect every part's parts.id/index/count and determine whether Switch or Catch discarded a part along the way. Configure a bounded count or timeout and a policy for incomplete data; rebuild the metadata if necessary.
  • Two batches are mixed together: Check whether parts.id or topic was overwritten manually. Reproduce the problem with two interleaved sets of Inject messages, and leave sequence IDs under the control of the node that creates them.
  • Sort appears to order numbers incorrectly: Confirm that the sort key is a number rather than a string, and test with 2 and 10. If the data comes from a parser, validate and convert it with Change first.
  • Memory usage keeps growing: Check pending Join, Sort, and Batch sequences, the Delay queue, and the input rate. Stop the test input, reduce the count, timeout, or window, and set a runtime buffer limit. Do not respond by adding memory alone.

Pinned Sources

If another document shows different UI options or defaults, check its Node-RED version first. Then import this chapter's safe example and use manual inputs to verify the routing and sequence behavior.

Frequently Asked Questions

Should I use Change or Function?
Prefer Change for straightforward set, replace, delete, move, and typed-mapping operations; it is easier to review on the canvas. Use Function only when explicit program logic is necessary, and follow the error-handling and lifecycle guidance in Chapter 16.
How can I prevent Join from mixing batches sent by two sources at the same time?
Preserve the parts.id that Split creates for each sequence; do not overwrite it with a fixed value. Then verify the behavior with two interleaved input sets. If the source has no reliable sequence boundary, first define a bounded grouping contract with a termination condition.
Can Range's clamp option replace input validation?
No. Clamp is a policy for out-of-range numbers; it does not establish that the source is trustworthy or the data structure is valid. Reject missing, non-numeric, and unreasonable data first, and then decide whether to scale it.
If I delete some parts after Split, will Join detect that automatically?
Not necessarily. The original parts.count may still specify the complete number of parts, causing Join to wait. Use sequence-aware rules or redesign the count and timeout policy, then test a sequence with a missing part.
Can I delete msg.parts to simplify the flow?
Not if a downstream Join, Sort, or Batch node still needs it. msg.parts is the sequence contract; remove it only after the sequence has explicitly ended and no downstream node requires it.