The Node-RED switch node is the simplest way to send each MQTT message down the right path: temperatures to one branch, humidity to another, out-of-range values to an alert. In this tutorial you will combine the switch node with the change node to route messages by topic and by value, reshape payloads without writing JavaScript, and republish clean alerts to a broker. You will finish with an importable flow you can test in two minutes.
Why Route MQTT Messages Inside Node-RED
A single mqtt in node subscribed to a wildcard such as sensors/# receives everything below that level. That is convenient, because one subscription replaces dozens, but it means every downstream node sees every message. Without routing you end up writing one large function node full of if statements that nobody wants to maintain.
The switch node solves this declaratively. It evaluates a property of the message against a list of rules and sends the message out of the port that matches. The change node then edits the message on each branch: it sets a new topic, moves a field, deletes noise, or computes a value with a JSONata expression. Together they cover most routing jobs, and the logic stays visible in the editor where a colleague can read it at a glance.
If you are new to the editor itself, start with the Node-RED MQTT tutorial and come back here once you have a broker connection working.
How the Node-RED Switch Node Works
Open a switch node and you will see three settings that matter.
- Property. The value to test. It defaults to
msg.payload, but it can bemsg.topic, a nested field such asmsg.payload.value, a flow or global context variable, or a JSONata expression. - Rules. Each rule creates one output port. Rules are evaluated in order.
- Checking mode. "Checking all rules" sends the message out of every matching port. "Stopping after first match" sends it out of the first match only. For routing, you almost always want the second option, so a value never lands in two branches by accident.
The rule types you will use most often are listed below.
| Rule | What it checks | Typical MQTT use |
|---|---|---|
==, != |
Exact equality | Device state such as ON or OFF |
<, <=, >, >= |
Numeric comparison | Threshold alerts |
| is between | Inclusive range | Normal operating band |
| contains | Substring | Topic contains /alarm/ |
| matches regex | Regular expression | Topic ends with /temperature |
| is true / is false | Boolean | Door open flags |
| is null / is not null | Missing values | Drop incomplete readings |
| is of type | string, number, object, buffer... | Reject malformed payloads |
| has key | Object has a property | Optional fields |
| JSONata exp | Any expression returning true | Multi-field conditions |
| otherwise | Nothing else matched | Catch-all and logging |
Always add an otherwise rule at the end. It costs nothing and it is the first place you will look when a message seems to disappear.
How the Change Node Works
The change node applies a list of operations to the message, in order:
- Set a property to a string, number, boolean, JSON, another message property, a context value, a timestamp or a JSONata expression.
- Change part of a string, with plain text or a regular expression, which is handy for rewriting topics.
- Delete a property, for example a debug field a device includes in every message.
- Move a property to a new location, such as lifting
msg.payload.valueup tomsg.payload.
JSONata is what makes the change node powerful. Inside an expression, payload and topic refer to msg.payload and msg.topic, so $round(payload.value * 9 / 5 + 32, 1) converts Celsius to Fahrenheit and $split(topic, "/")[4] extracts the fifth topic level. You get computed values without a function node and without leaving the visual editor.
The Example Flow: Route by Topic, Then by Value
The flow you are about to import does the following:
- Subscribes to
connectmq/demo/a7f3k2/sensors/#on the public MQTT test broker with the payload type set to parsed JSON. - A first Node-RED switch node routes by
msg.topic: temperature, humidity, or anything else. - A second switch node splits temperatures into three bands on
msg.payload.value. - Low and high bands go through a change node that builds an alert topic and payload, then an
mqtt outnode publishes it. - Normal readings, humidity and unknown topics go to debug nodes.
Replace a7f3k2 with your own random string everywhere before deploying. The public broker is readable by anyone, so a unique prefix keeps your test traffic separate, and you should never send sensitive data there. The client ID must also be unique and non-empty.
[
{"id":"cmq_broker","type":"mqtt-broker","name":"ConnectMQ public","broker":"broker.connectmq.com","port":"1883","clientid":"nr-switch-a7f3k2","autoConnect":true,"usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true},
{"id":"in_all","type":"mqtt in","name":"all sensors","topic":"connectmq/demo/a7f3k2/sensors/#","qos":"1","datatype":"json","broker":"cmq_broker","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":160,"wires":[["sw_topic"]]},
{"id":"sw_topic","type":"switch","name":"by measurement","property":"topic","propertyType":"msg","rules":[{"t":"regex","v":"/temperature$","vt":"str","case":false},{"t":"regex","v":"/humidity$","vt":"str","case":false},{"t":"else"}],"checkall":"false","repair":false,"outputs":3,"x":350,"y":160,"wires":[["sw_temp"],["dbg_hum"],["dbg_other"]]},
{"id":"sw_temp","type":"switch","name":"temperature band","property":"payload.value","propertyType":"msg","rules":[{"t":"lt","v":"5","vt":"num"},{"t":"btwn","v":"5","vt":"num","v2":"30","v2t":"num"},{"t":"gt","v":"30","vt":"num"},{"t":"else"}],"checkall":"false","repair":false,"outputs":4,"x":560,"y":100,"wires":[["chg_alert"],["dbg_ok"],["chg_alert"],["dbg_other"]]},
{"id":"chg_alert","type":"change","name":"build alert","rules":[{"t":"delete","p":"payload.debug","pt":"msg"},{"t":"set","p":"payload","pt":"msg","to":"{\"room\": $split(topic, \"/\")[4], \"value\": payload.value, \"level\": payload.value > 30 ? \"high\" : \"low\", \"fahrenheit\": $round(payload.value * 9 / 5 + 32, 1)}","tot":"jsonata"},{"t":"set","p":"topic","pt":"msg","to":"\"connectmq/demo/a7f3k2/alerts/\" & payload.room","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":60,"wires":[["out_alert","dbg_alert"]]},
{"id":"out_alert","type":"mqtt out","name":"publish alert","topic":"","qos":"1","retain":"false","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"cmq_broker","x":1000,"y":40,"wires":[]},
{"id":"dbg_alert","type":"debug","name":"alert","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":990,"y":90,"wires":[]},
{"id":"dbg_ok","type":"debug","name":"normal temperature","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":800,"y":120,"wires":[]},
{"id":"dbg_hum","type":"debug","name":"humidity","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":560,"y":200,"wires":[]},
{"id":"dbg_other","type":"debug","name":"unrouted","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":800,"y":240,"wires":[]}
]
Import it with Menu → Import, paste the JSON, then click Deploy. The mqtt in node should show a green "connected" status under it within a few seconds.
Notice two details in the change node. The payload is rebuilt first, and the topic rule then reads payload.room from the new payload. Rule order matters: each rule sees the message as the previous rule left it. Also notice that the mqtt out node has an empty topic field, so it publishes to whatever msg.topic the change node set. If you fill in the topic field on the node, it overrides msg.topic and your dynamic routing silently stops working.
Test the Routing from the Command Line
Publish a few test messages with the Mosquitto clients. Each command uses its own client ID.
# Normal temperature -> "normal temperature" debug output
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-a7f3k2-1 \
-t connectmq/demo/a7f3k2/sensors/room1/temperature -m '{"value": 22.5}'
# High temperature -> alert published to connectmq/demo/a7f3k2/alerts/room1
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-a7f3k2-2 \
-t connectmq/demo/a7f3k2/sensors/room1/temperature -m '{"value": 34.2, "debug": "fw-1.2"}'
# Humidity -> "humidity" debug output
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-a7f3k2-3 \
-t connectmq/demo/a7f3k2/sensors/room1/humidity -m '{"value": 48}'
# Unknown measurement -> "unrouted" debug output
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-a7f3k2-4 \
-t connectmq/demo/a7f3k2/sensors/room1/co2 -m '{"value": 640}'
In a second terminal, watch the alert topic to confirm the change node built the message you expected:
mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-a7f3k2 -v \
-t 'connectmq/demo/a7f3k2/alerts/#'
You should see {"room":"room1","value":34.2,"level":"high","fahrenheit":93.6}, with the debug field gone. If you are unsure what each QoS setting does to delivery in this test, the guide to MQTT QoS levels explains why the flow uses QoS 1 for alerts.
Routing Patterns Worth Reusing
Once the basic flow works, these patterns cover most real projects:
- Route by device type with a topic level. Instead of a regex, set the switch property to the JSONata expression
$split(topic, "/")[3]and compare it with==rules. This is faster to read than a regex when topics follow a strict hierarchy. - Validate before routing. Put a switch node directly after
mqtt inwith an "is of type: object" rule. Malformed payloads go to theotherwiseport, where you can log them instead of letting them crash a function node later. - Deduplicate state changes. Pair the switch node with a filter node (report-by-exception mode) so a door sensor that publishes
openevery second only triggers your branch when the value changes. - Normalise units early. Use a change node right after routing to convert units and rename fields, so everything downstream sees one schema.
- Enrich with context. A change node can set
msg.payload.limitfromflow.limits[room], and the next switch node can compare againstmsg.payload.limitinstead of a hard-coded number. Thresholds then live in one place.
When conditions grow beyond a handful of rules, or need loops and state, a function node is the better tool. The Node-RED function node guide covers that step, and the JSON node tutorial explains how string and object payloads interact with these routing rules.
Troubleshooting Switch and Change Nodes
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing leaves the switch node | Payload is a string, not an object | Set mqtt in output to "a parsed JSON object" or add a JSON node |
| Numeric rule never matches | Value arrives as "34.2" (string) |
Convert with a change node: $number(payload.value) |
| Message appears in two branches | "Checking all rules" is selected | Switch to "stopping after first match" |
| Alerts go to the wrong topic | Topic field is filled in on mqtt out |
Leave it empty so msg.topic is used |
| JSONata error in the debug sidebar | Expression references msg.payload |
Inside JSONata, use payload, not msg.payload |
| Regex matches too much | Missing anchor | End patterns with $, for example /temperature$ |
A useful habit: give every switch and change node a descriptive name. The debug sidebar shows the node name, and "temperature band" is far easier to trace than "switch".
Moving the Flow to a Private Broker
The public broker is ideal for learning, but production routing flows should run against your own broker, with credentials and TLS. Create a broker in the ConnectMQ portal, then edit the mqtt-broker config node:
- Server: the host shown in the portal, port
8883, Use TLS enabled. - Security tab:
YOUR_USERNAMEandYOUR_PASSWORD. - Client ID:
YOUR_CLIENT_ID, because ConnectMQ binds client IDs to the credentials it issues.
Every mqtt in and mqtt out node shares that config node, so the routing logic itself does not change. You can then drop the connectmq/demo/a7f3k2 prefix and use a proper hierarchy such as site/area/line/device/measurement. Running it on a fully managed MQTT service also means you are not patching and monitoring a broker process next to Node-RED.
Run Your Node-RED Flows on ConnectMQ
Your switch and change nodes deserve a broker that stays up while you iterate. Sign up at app.connectmq.com, choose the free Serverless plan (no credit card), create a broker and paste the generated username, password and client ID into your Node-RED config node. The portal's live topic explorer shows your alert topics arriving in real time, which makes checking routing logic much faster.
Frequently Asked Questions
What is the difference between the switch node and the change node in Node-RED?
The switch node decides where a message goes by testing a property against rules and choosing an output port. The change node modifies the message itself by setting, changing, deleting or moving properties.
Can the Node-RED switch node route on msg.topic?
Yes. Set the property to msg.topic and use "matches regex", "contains" or == rules. This is the standard way to split traffic from a single wildcard MQTT subscription.
Why does my numeric switch rule not match MQTT values?
MQTT payloads arrive as text unless the mqtt in node parses them. Make sure the node outputs a parsed JSON object, and convert string fields with $number() in a change node before comparing.
Should I use a function node instead of switch and change nodes?
Use switch and change nodes for routing and simple transformations, because they are easier to read and review. Move to a function node when you need loops, complex state or logic that would take more than a few rules.



