msg carries more than payload, and guessing what type is inside gets expensive fast
Part 2 ended with a flow that does nothing to your house: Inject → Change → Debug, one message in, one line out. That message was never just its payload. Every wire in Node-RED carries a JavaScript object called msg, and payload is only the property most core nodes happen to look at by default — not a synonym for the whole thing, and not a guarantee of what type is sitting inside it. This part opens that object properly: what belongs where, what the runtime copies and what it deliberately does not, and then the three nodes — Change, Switch, and Range — that let you reshape and route a message without writing a line of JavaScript. It ends with the two sequence nodes, Split and Join, that earn their place only when you genuinely need them, because reaching for them out of habit is how a flow quietly sits waiting for a message that is never coming.
A flow can fail without a single node crashing
Home automation flows rarely break because a node throws an error. They break because one node sent the string "21.5" and the next node ran a numeric comparison against it, or because the value you actually wanted was never at msg.payload in the first place — it was three levels deeper, at a path shaped like msg.data.new_state.state. Inspecting the message before you wire a condition against it is the difference between catching that in five minutes and debugging a flow that “looks right” for a week.
Everything in this part is checked against Node-RED 5.0.2, the runtime bundled inside Add-on 22.0.1 — the same pinned baseline the rest of this guide uses. Interface labels can move between locales and patch versions, so the instructions lean on stable node and property names rather than exact menu wording. Every example here uses fictional indoor sensor data — temperature and humidity readings that connect to no real device and trigger no Home Assistant action. That is the same discipline Part 2 used for its first flow: get the mechanics right before anything downstream can actually turn something on or off.
topic, parts, error information, and any custom property you added yourself. Nothing about the name “payload” makes it special beyond convention — it is simply the property most core nodes read by default.{"payload":{"temperature":23.4},"topic":"example/room"} — payload is an object, payload.temperature is a number, topic is a string. All three facts matter, and none of them is visible just by glancing at the wire.msg.payload silently does nothing when the real value sits at msg.data.new_state.state instead. The flow does not error. It just never matches, and nothing on the canvas tells you why.Structure is the path, type is what sits at the end of it
- Message object. A Node-RED node receives and sends a plain JavaScript object. Its properties can hold primitives, objects, arrays, or Buffers, and each node only documents the properties it actually reads or writes — nothing more.
- payload. The property most core nodes process by default. It is not a fixed schema, and it is not another name for
msgitself. - topic. A category label whose meaning you assign yourself — “living-room temperature” versus “study temperature,” say. Some nodes create or preserve it; plenty do not. Never assume it survived a hop you didn't check.
- _msgid. A tracking id the runtime generates for any message that arrives without one. Useful for following a message through Debug while you build a flow. Not a business key, a device id, or a security token — more on why below.
- Structure versus type. Structure is the property path —
payload.temperatureexists or it does not. Type is what kind of value sits there once you arrive — a number, or a string that merely looks like one. A condition can fail with the correct path and the wrong type, and that failure looks identical to a missing path unless you check both separately.
msg is the whole parcel that arrives at a node — payload is one item inside it, sitting next to the address label (topic) and the tracking sticker the courier stuck on without asking (_msgid). A node that only ever opens the one item and never looks at what else is in the box will process an almost-empty parcel just as happily as a full one, and never notice that topic and _msgid were sitting right there the whole time.
If you ever need your own way to correlate messages across a flow, create a clearly named property from a source you control — msg.correlation, say — and put a limit on its length and the characters it can contain. Do not overwrite _msgid to imitate another message, and do not search Debug output by id and then treat that id as something permanent. It is not guaranteed to survive reinjection, a Split, or being handled by any other node along the way.
When a message fans out across more than one wire, the runtime does not hand every wire the same object. The first wire downstream can reuse the message as it stands; every wire after that receives a message the runtime clones first. That distinction only bites the moment you start mutating things — “assign msg.payload to another property” and “make an independent copy of it” are not the same operation, because objects and arrays in JavaScript are reference types. A shallow assignment can leave two properties pointing at the same object, so a later change to one shows up in the other whether you meant it to or not.
flowchart LR A["A message reaches a
node with three wires"] --> B["Wire 1: the same
message object, reused"] A --> C["Wire 2: a cloned copy"] A --> D["Wire 3: another cloned copy"] C --> E["Deep-copied, property
by property — except
msg.req and msg.res,
which stay live"] D --> E
msg.req and msg.res, which the runtime deliberately leaves as the same live objects even inside a clone.The Change node's set operation has a deep-copy option for exactly this reason, and the core cloning utility performs a real deep copy on ordinary message data whichever way you trigger it. Whichever method you use, put the source and the destination side by side in Debug afterward and confirm they really did separate. Do not modify an object inside a Function node if you intend to keep sending the original afterward — that mutation is visible to anyone else still holding a reference to it.
msg.payload = { temperature: 23.4 };msg.topic = "example/room";return msg;That is the shape of the object, not an instruction to reach for a Function node — the operations above map cleanly onto Change, covered next. Leave msg._msgid for the runtime to manage; do not set it yourself in code like this.
msg.req and msg.res. Node-RED's cloning utility deliberately keeps these as the same live objects rather than deep-copying them, because they cannot be meaningfully duplicated. Do not put either one in context, serialize it, pass it through a branch that doesn't need it, or let it show up in Debug output. The HTTP request-and-response lifecycle gets its own treatment later in this guide.A five-step routine that turns guessing into a habit
The fastest way to lose an evening is writing a condition against a path you assume is there, deploying, and then wondering why nothing fires. Here is the routine this guide's own examples are built with — side-effect-free, using fictional sensor values, and worth running once by hand before you trust it as a habit.
-
Step 1
Build an input with nothing riding on it
Add an Inject node and give it
{"temperature":23.4,"humidity":58}as a JSON typed value. Set Topic toexample/room. Both are fictional — they identify no device and connect to nothing real. -
Step 2
Inspect the whole msg first, not just payload
Wire a Debug node to it and configure its output to show the complete message, not only
msg.payload. Trigger the Inject node once, expandpayloadin the sidebar, and read the type badge on every property — including the_msgidthe runtime just added. -
Step 3
Narrow to the exact path you actually need
Duplicate that Debug node and point it at
msg.payload.temperaturespecifically. Confirm the badge reads number. If it reads string instead, go back and fix the typed value at the Inject node itself — do not paper over it with a looser comparison three nodes downstream. -
Step 4
Map without throwing away the original
Add a Change node that copies
msg.payload.temperaturetomsg.measurement.value, and writes the literal string°Ctomsg.measurement.unit. Leave the original payload in place so you can compare the shape before and after the mapping, side by side. -
Step 5
Deliberately break it, twice
Add two more Inject nodes: one that omits
temperatureentirely, one where it is a string instead of a number. Wire both only to Debug or Switch — never to anything that would act on a real device. Confirm your flow actually tells apart a valid reading, a missing one, and one with the wrong type, rather than treating all three the same.
Change only one condition at a time, and trigger each Inject node by hand rather than in a batch. It is slower than it sounds, but it is what gives you a message contract you can actually reproduce — and it is what makes the difference obvious later, when you replace this fictional input with a real Home Assistant state event.
Step 3 above is really a decision, and it comes up constantly once real data starts arriving:
flowchart TD A["A condition you expected
to match does not fire"] --> B{"Open Debug on the complete
msg. What type badge does
the value actually show?"} B -->|"number, as expected"| C["The condition itself is wrong.
Recheck the comparison value"] B -->|"string, not number"| D["Fix it at the source —
Inject, Change, or a parser —
not by loosening the rule"] B -->|"undefined"| E["The path doesn't exist. Check
each parent property, one
level at a time"]
Define what a message must contain before you wire anything against it
A well-designed flow states its contract at the boundary: which properties are required, which types are accepted, and where the result gets written. “This flow requires a number at msg.payload.temperature, a number at msg.payload.humidity, and a string at msg.topic, and it writes a summary object to msg.payload” is a contract. “This flow receives a payload” is not — it is a shrug.
| Property | Example type | Purpose | Failure handling |
|---|---|---|---|
msg.payload.temperature | number | Fictional indoor-temperature value | Send missing or non-finite values to a validation branch |
msg.payload.humidity | number | Fictional relative-humidity value | Stop processing values outside the reasonable test range |
msg.topic | string | Data-source category | Send an unknown topic to the else branch |
msg._msgid | string | Short-term diagnostic correlation | Observe it only; do not set it yourself |
topic earns its keep through consistency, nothing else. A Switch node can route different rooms to separate branches based on msg.topic, and a Trigger node can keep a separate timer per topic — but only if every upstream node agrees on what goes in that field. The moment one node uses an entity id and another uses a display name for the same thing, that routing becomes unpredictable. Pick one stable, non-sensitive, verifiable value and normalize it centrally in a single Change node, rather than trusting everyone downstream to remember the convention.
example, or this guide's convention sensor.YOUR_SENSOR — before you export a flow or paste one into a forum post. Credentials, authorization headers, and access tokens must never appear in a message example or a Debug screenshot, full stop.Objects, arrays, and the three ways “nothing” shows up
- Object. Reached through named properties like
payload.temperature. Confirm the parent actually exists and is an object before you trust the child. - Array. Indexes start at 0. An empty array, a missing index, and an element whose value is
nullare three different conditions — do not let a Switch rule collapse them into one. - undefined. Usually means the path doesn't exist at all. It is not the same thing as JSON's
null, and it may not even show up as a property once the message is serialized. - Mutation. Assigning straight into
msg.payload.temperaturechanges the current message in place. If you need the original for later comparison, map the derived value into a new, clearly named property instead of overwriting it.
Keep source events and derived results apart on purpose — preserve msg.payload and put a calculated result in msg.analysis, for instance. Before you sort an array or hand it to Split or Join, put a hard limit on how many elements it can contain; without one, untrusted input can leave a sequence node holding a large number of messages in memory for a long time. And in a flow with several branches, do not assume “one branch deletes a property first, so the other branch never sees it” — treat every wire's input as its own independent contract. Where real isolation matters, use an explicit mapping or a deep copy, then check both paths at once with two separate Debug nodes.
The type selector, and why identical-looking values aren't the same value
The small type selector next to a field in Inject, Change, and Switch decides how the editor interprets what you typed. String, number, Boolean, JSON, timestamp, and a msg/flow/global property are the usual options; some nodes add environment variables or a JSONata expression. What is actually offered depends on the node in front of you — trust the editor over any general list, including this one. Type 23 and select string, and you still get a string. Two values that look identical on screen are not necessarily the same type underneath.
| Value | Type | Considerations |
|---|---|---|
"off" | string | A common Home Assistant state string; it is not the Boolean value false |
false | boolean | Can be evaluated directly as a Boolean |
0 | number | Do not use it interchangeably with the string "0" |
{"value":23.4} | object, after JSON parsing | Limit its size and validate its properties first |
[1,2,3] | array | Set an element limit before passing it to Split |
The string "off" is a sticky note that has the word “off” written on it. The Boolean false is the switch actually being off. A Switch rule that expects true or false and gets handed the sticky note instead will not fail loudly — it will quietly treat the note as truthy, because a non-empty string is truthy in JavaScript, sticky note and all. Reading the type badge in Debug is how you tell which one you are actually holding.
Parsers change the shape. They do not vouch for the content.
Node-RED 5.0.2's core registers five parser nodes: CSV, HTML, JSON, XML, and YAML. Each one converts one representation into a different data structure — the JSON node moves between a JSON string and a JavaScript value, the HTML parser can emit several messages carrying msg.parts, and CSV, XML, and YAML each carry their own structural options. None of that is the same claim as “safe to use.” Successful parsing only proves the representation was well-formed.
Processing a response from an external weather service is the pattern worth memorizing: limit its size first, then run the appropriate parser, then use a Switch node to check the properties and types you actually require, and only then map it into your own structure. HTML selectors, CSV columns, and YAML layouts can all change the moment their source changes. Keep a Catch path wired in, and sanitize anything from that response before it ends up in an error message anyone else will read.
Four operations, run in the order the editor shows them
Change is the node for moving fields, filling in defaults, and normalizing a structure — the cases where reaching for a Function node would be using a sledgehammer on a thumbtack. The 5.0.2 editor gives it four operations: set, change, delete, move. Set and change both accept a typed value, and set additionally offers a deep-copy option for exactly the reference-type problem covered in the previous section.
| Operation | Source | Destination |
|---|---|---|
| set | msg.payload.temperature | msg.measurement.value |
| set | string °C | msg.measurement.unit |
| set | msg.topic | msg.measurement.source |
| delete | msg.authorization | Removes a sensitive field before the message enters a general-purpose diagnostics branch |
Rule order changes the result, because each rule runs against whatever the message looks like after the rule before it. Move payload before you read payload.temperature in a later rule, and that later rule finds nothing — the path it wanted is already gone. After editing a set of rules, compare the complete message in Debug rather than trusting that it did what you meant. If you are copying an object to another property and both copies might get changed independently later, turn on deep copy and then actually verify that a change to the nested copy does not show up in the original.
Change can also write into flow and global context — the fact that it can does not mean it should. Keep temporary derived values inside msg wherever you can, and reach for context only for state that genuinely needs to survive across messages, with its scope, its limits, and its restart behavior defined explicitly (that gets its own treatment later in this guide). Credentials, whole event objects, and unbounded arrays do not belong in context, ever.
“Map measurement fields” tells the next person what a node does at a glance. “Change 1” tells them nothing. Use the node's description field to record what types it accepts and where a missing value goes, and if a single node's rule list gets hard to read, split it into two — “Map validated input” and “Clean public output” is a reasonable way to divide that.
One setting decides how many outputs a single message can leave through
A Switch node's property field takes the same range of types the editor offers elsewhere — msg, flow, global, and an expression among them. Build the rule list in a specific order: start with a rule that catches a missing property or a type mismatch, then define the actual business ranges you care about, and finish with an else. Route every output to a Debug node while you're testing — not straight into a heating or air-conditioning Action node.
| Test input | Expected route | Mistake to avoid |
|---|---|---|
| number 23 | Normal range | Confusing it with string “23” |
| Missing property | Missing-value branch | Treating it as normal after it falls through to else |
null | No-value branch | Treating it as 0 |
| Extreme number | Out-of-range branch | Passing it to Range without validation |
“Check all rules” and “stop after first match” decide something that matters more than it looks: whether one message can leave through more than one output at once. Two rules like more than 20 and more than 25 can both be true for the same number, and if the node is set to check every rule, that number leaves through both outputs. If any one of those outputs eventually causes a real side effect, design the ranges so they cannot overlap in the first place, then prove it with a test matrix rather than trusting the visual order the wires happen to leave the node in.
flowchart TD A["A Switch node with two
overlapping rules, such as
more than 20 and more than 25"] --> B{"Is check all rules
turned on?"} B -->|"off, stop at first match"| C["One message leaves through
exactly one output"] B -->|"on"| D["A value matching both rules
leaves through both outputs"]
Range: four policies for a number outside the range you configured
In the 5.0.2 source, Range's action options are scale, clamp, roll, and drop. Say you're mapping a 0–100 brightness percentage down to an internal 0–1 range. Scale keeps applying the same linear mapping past the edges you configured. Clamp pins an out-of-range result to whichever endpoint it's closest to. Roll wraps the value back around, the way a compass heading does. Drop sends nothing at all for a value outside the range. None of the four is universally correct — each is a decision about your specific flow. For home control specifically, rejecting an anomalous reading before it ever reaches Range is generally the safer call than reaching for clamp to paper over a sensor that is already misbehaving.
flowchart TD A["A value arrives outside
the configured input range"] --> B{"Which Range action
is set?"} B -->|"scale"| C["The linear mapping
continues past the range"] B -->|"clamp"| D["The result is pinned to
the nearest output endpoint"] B -->|"roll"| E["The value wraps
around the range"] B -->|"drop"| F["No message is
sent at all"]
Scale is a speedometer needle that keeps climbing past the last printed number on the dial. Clamp is the same needle pinned against the peg at the end of its travel, refusing to go further. Roll is a compass — spin past 360° and you're back at 0°, not off the edge of the dial. Drop disconnects the needle entirely: no reading at all, rather than a wrong one. Choosing between them is choosing what you'd rather see when the input is already wrong.
Import and test the safe routing example
This guide's source material ships a small example flow built entirely from Inject, Switch, and Debug — no credentials, no Home Assistant server, no node with a side effect. Running it once by hand is a faster way to internalize check-all-rules than reading about it.
-
Step 1
Read the flow JSON before you import it
Open 02-switch-routing.json and confirm it contains exactly what it claims to: one tab, and Inject, Switch, and Debug nodes — no credentials, no Home Assistant server, no network endpoint, nothing with a side effect.
-
Step 2
Import it into a fresh test tab
Use the editor's Import feature and paste in the file's contents. Look over all five nodes again once they land on the canvas. Never deploy a stranger's JSON you haven't actually read.
-
Step 3
Trigger the matching route
The example's Inject node sends the string
開啟(“turn on”). Switch readsmsg.payload: its first rule is a string comparison, its second is else. Trigger the Inject node once — only the Debug node on the matching branch should show anything. -
Step 4
Build your own counterexamples
Duplicate the Inject node and try
關閉(“turn off”), an empty string, and a plain number, one at a time. Confirm each one takes the other route, rather than assuming anything that merely looks similar in the editor counts as equal. -
Step 5
Add normalization with Change
Insert a Change node ahead of Switch, move the test input to
msg.command, and set a fixedmsg.topic. Check every rule's typed value individually, deploy, and rerun all four test cases again.
Disable any Debug nodes you no longer need once you're done. Nothing in this example calls a Home Assistant Action — 開啟 is just text here. Controlling a real device still needs an allowlist, state preconditions, and an explicit target, all of which come later in this guide.
A sequence is a relationship between several messages, and holding it open costs memory
If conditional routing with Switch and Range is already solving your problem, skip this section entirely and come back the day you actually need to process an array item by item. A sequence in Node-RED is Split producing several messages tagged with msg.parts, and Join, Sort, or Batch holding messages and eventually emitting a result once that metadata — or a manually configured condition — says it's time. Every message any of those three nodes is holding onto sits in memory until it's released, so an unbounded source, or one that never sends a proper ending message, is a real resource problem, not a theoretical one.
Split: one message becomes several, with a contract attached
Split can divide a string, an array, an object, or a Buffer, and it creates msg.parts to describe the resulting sequence. For an array, that means an id, an index, a count, and a length; an object adds a key; a string or Buffer carries type and delimiter information instead. If the input already carried its own parts, Split nests the old ones in a stack so Join can reconstruct a sequence with more than one level.
Here is the failure mode worth internalizing before you build anything real: inject a fictional array of, say, five room readings, split it into individual messages, use Switch to drop the invalid ones, remap fields with Change, then try to Join the rest back into an array. If Switch drops even one part, an automatic Join can wait forever for a message that is never coming — because parts.count still says the original count, five, regardless of how many parts actually survived. Switch does have sequence-aware rules and pending-group behavior built in, but that does not remove your responsibility to define a policy for a part that goes missing. Do not assume arbitrary filtering repairs the sequence metadata on its own, because it does not.
sequenceDiagram participant S as Split participant W as Switch participant J as Join, automatic S->>W: 5 parts, count set to 5 W->>J: 4 parts, one filtered out J->>J: waits for the missing 5th Note over J: parts.count still says five,
so Join never declares done
parts.count value it started with still says five.It is a kitchen that promised five courses and only sent out four, because the fifth one burned. The dining table — Join, in automatic mode — was told to expect five, and nobody updated it when the count changed. So it sits there, plates ready, waiting for a course that already went in the bin. Nothing is technically broken; nobody told the table the plan changed.
Join itself works in automatic, manual, or reduce mode, using the options the 5.0.2 editor actually shows you. Automatic reconstructs data purely from what Split's metadata says. Manual needs an explicit count, a timeout, or a completion signal from you. A stream with no defined end is not something you should let Join wait on indefinitely — and even with a timeout set, you still have to decide what happens to the partial data: discard it, mark it incomplete, or route it to its own isolation branch.
nodeMaxMessageBufferLength is a last-resort safeguard, not a substitute for keeping the flow itself bounded in the first place.At minimum, run an empty array, a single element, an ordinary multi-element array, a sequence missing one part, two interleaved sequences, and a duplicate index through whatever you build. Inspect msg.parts.id, msg.parts.index, and msg.parts.count in Debug at each stage, and strip anything environment-specific before you share the output with anyone.
Sort and Batch: ordering and grouping, with the same metadata underneath
Sort can order an array inside a single message, or collect and order an entire sequence using msg.parts — in sequence mode it checks that parts carries at least an id and an index, then rewrites the indexes once sorting is done. State the sort key's type and direction explicitly: sorting numbers as strings gives a different answer than sorting them as numbers, and if equal keys need a stable order, verify that with real test data rather than trusting whatever order messages happened to arrive in.
Batch in 5.0.2 supports count-based overlapping batches, time-interval batches, and concatenating a sequence by topic. It creates or updates msg.parts itself, and some of its modes need a complete id, index, and count to work at all. An overlapping window putting the same message into more than one batch is the configured behavior, not a duplicate-delivery bug — and whether a timed batch emits an empty sequence when nothing arrived depends entirely on the options you chose. Do not guess from the option's name; test it with Inject before you rely on it.
msg.parts field | Role | Common failure |
|---|---|---|
id | Distinguishes concurrent sequences | Overwriting it manually mixes two batches |
index | Identifies the part's position, usually starting at 0 | Filtering leaves gaps, duplicates, or an incorrect order |
count | Records the known total number of parts | The declared number of parts never arrives |
type / key / ch / len | Describes the original container and reconstruction details | Change deletes it accidentally, so Join reconstructs the wrong structure |
parts | Stores the nested sequence stack | The hierarchy is misunderstood after multiple Split operations |
Batch is a reasonable way to collect test-only notifications from a short window into a small summary. It is not a place to delay a safety alert, and its window should never be allowed to grow unbounded. After sorting or batching anything, check count, index, and payload in Debug before it reaches an external notification node — and do not assume a sequence held in memory will survive a flow restart. Either let the source resend it, or discard the incomplete batch safely rather than guessing at what it would have contained.
The failures that actually happen, matched to the fix that actually works
| Symptom | Likely cause | What to do |
|---|---|---|
| Switch shows 23 in Debug, but a numeric rule doesn't fire | The value's type badge reads string, not number | Go back to Inject, the parser, or Change and produce an actual number — don't loosen the rule with a looser comparison instead |
| A nested property shows undefined | A parent object along that path doesn't exist, or the property name is misspelled | Show the complete msg in Debug and check payload, every parent, and the spelling one level at a time. Also test an input whose parent is null |
| Changing one branch makes another branch's result unstable | You're relying on branch order, or on a shared reference instead of an independent contract | Map the source into a new property or take a deep copy, then compare both paths with two separate Debug nodes. Never patch a data race with a Delay node |
| A parser errors, or its output structure suddenly changes | The source format changed, or you're testing against a different parser mode than the one configured | Save a small, de-identified test string, confirm the parser's mode and output property, and wire in a Catch node. Never publish a full external response or its headers while debugging |
| Debug shows req, res, or authorization data | Complete-message Debug output is on for a node that carries live HTTP objects | Turn off complete-message Debug output immediately, delete any sidebar or log export that captured it, and rotate any exposed credential through your normal incident process — never post it to a forum to ask for help |
| Switch sends one message out two outputs at once | Two rules overlap, and check-all-rules is turned on | Make the ranges mutually exclusive, then rerun your boundary tests. Do not rely on wire position to prevent a side effect |
| Nothing comes out of Range | The property is missing or not a number, or the action is set to drop | Compare Debug before and after Range. Don't switch to clamp just to make a source-data error go quiet |
| Join waits forever | Something upstream — Switch or a Catch path — discarded a part, but parts.count still describes the original total |
Check parts.id/index/count on every survivor, set a bounded count or timeout with an explicit policy for incomplete data, and rebuild the metadata if you have to |
| Two batches get mixed together | parts.id or topic was overwritten by hand somewhere upstream |
Reproduce it with two genuinely interleaved sets of test messages, and leave sequence ids under the control of whichever node created them |
| Sort seems to get numbers in the wrong order | The sort key is being read as a string, not a number | Confirm the key's actual type, test specifically with 2 and 10, and validate/convert with Change first if the data came from a parser |
| Memory keeps climbing during testing | Pending Join, Sort, or Batch sequences, a growing Delay queue, or an input rate you haven't bounded | Check every pending sequence and the Delay queue, stop the test input, tighten the count/timeout/window, and set a runtime buffer limit. Don't just add memory and hope |
The ones that come up again and again
Should I overwrite the original payload if a later node still needs to compare it?
Can I set _msgid myself and use it as a device id?
A source sometimes sends a number and sometimes a numeric string. Can I just compare them directly?
Is it safe to hang onto req/res after copying msg?
Does JSON parsing succeeding mean the data is safe?
Should I reach for Change or Function?
How do I stop Join mixing up two sequences that arrive at the same time?
Can Range's clamp option stand in for validating the input?
If I delete some parts after Split, will Join notice automatically?
Can I just delete msg.parts to tidy up the flow?
Where to go from here
You can read a message honestly now, and reshape it without a Function node. Nothing downstream has changed state once.
Part 4 closes out Part 1 of this guide: Delay and Trigger, the two function nodes held back from this part, plus flow and global context done safely. Then comes the moment this whole foundation has been building toward — the Home Assistant nodes themselves, and the first flow that actually touches a real entity.
Open the full guidePart 3 of the WoowTech Complete Node-RED Guide series on the Apporo blog.
Adapted from the WoowTech Complete Node-RED Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo