Node-RED

Node-RED MQTT Topic Mapping: Remap Flat Device Topics into an ISA-95 Hierarchy

Learn how to remap messy, flat device topics into a structured ISA-95 namespace with a Node-RED lookup table, normalise payloads on the way, and republish everything to a ConnectMQ broker without creating loops.

Node-RED MQTT topic mapping flow converting flat device topics into an ISA-95 topic tree on a cloud broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

Node-RED MQTT topic mapping is the quickest way to turn the flat, vendor-specific topics your devices already publish into a clean ISA-95 hierarchy that the rest of the plant can understand. In this tutorial you will build a small Node-RED flow that subscribes to raw device topics, looks each device up in a mapping table, normalises the payload and republishes it to a structured namespace on a ConnectMQ broker.

Why Remap MQTT Topics at All?

Most brownfield installations grow organically. A temperature gateway publishes to dev/th-0042/temp, a power meter uses meter7/kw, and a PLC gateway writes plc/line2/status. Each topic made sense to whoever configured that device, but together they tell a consumer nothing about where the data lives in the plant.

A Unified Namespace fixes this by agreeing on one hierarchy, typically following ISA-95 levels: enterprise, site, area, line, cell or machine, and finally the data point. When every message sits at a predictable path such as acme/izmir/packaging/line2/filler1/temperature, dashboards, historians and analytics tools can subscribe with wildcards instead of maintaining a long list of exceptions. The background on this pattern is covered in our guide to Sparkplug B and the Unified Namespace.

You often cannot reconfigure the devices themselves. Firmware may be locked, the vendor may be gone, or touching a production controller requires a change window you will not get this quarter. Node-RED sits in the middle and does the translation without touching the source.

Designing the Target ISA-95 Topic Structure

Decide the target structure before writing any code. A mapping flow is only as good as the namespace it writes into. A practical layout looks like this:

Level Example value Notes
Enterprise acme Often implied by the broker or a fixed prefix
Site izmir One per physical plant
Area packaging Department or hall
Line line2 Production line or process unit
Machine filler1 Cell, machine or asset
Data point temperature One measurement or state per topic

A few rules keep the namespace healthy over time:

  1. Use lowercase, ASCII and no spaces, so topics are easy to type in subscriptions and filters.
  2. Publish one data point per topic, or one well-defined JSON object per machine, and keep the choice consistent.
  3. Never put timestamps, sequence numbers or values into the topic itself.
  4. Keep depth under control. On the public MQTT test broker at broker.connectmq.com a topic can have at most 8 levels, so this tutorial uses a three-level demo prefix plus five ISA-95 levels.

For the demo we will use connectmq/demo/ab12cd as the prefix (replace ab12cd with your own random string), raw input under connectmq/demo/ab12cd/raw/#, and structured output as connectmq/demo/ab12cd/<site>/<area>/<line>/<machine>/<point>. Remember that the public area is readable by everyone, so only send test data there.

How Node-RED MQTT Topic Mapping Works

The flow has four parts:

  1. An mqtt in node subscribes to the raw topics with a wildcard.
  2. A function node splits the incoming topic, finds the device in a lookup table, builds the new topic and normalises the payload.
  3. An mqtt out node publishes the result. If its own topic field is empty, it uses msg.topic, which is exactly what we want.
  4. A second output of the function node sends unmapped messages to a debug node so you can spot new or misconfigured devices.

If you are new to the MQTT nodes themselves, start with the Node-RED MQTT tutorial and come back here.

The mapping table

Keep the mapping in one place. For a handful of devices a JavaScript object inside the function node is fine. For dozens, load it from a file or a retained MQTT message into global context so you can update it without redeploying.

The function node

Paste this into a function node and set it to 2 outputs:

// Raw topics look like: connectmq/demo/ab12cd/raw/<deviceId>/<rawPoint>
const PREFIX = "connectmq/demo/ab12cd";

// deviceId -> ISA-95 location
const devices = {
    "th-0042":  { site: "izmir", area: "packaging", line: "line2", machine: "filler1" },
    "meter7":   { site: "izmir", area: "utilities", line: "main",  machine: "meter7" },
    "plc-l2":   { site: "izmir", area: "packaging", line: "line2", machine: "plc" }
};

// raw point name -> normalised point name and unit
const points = {
    "temp":   { name: "temperature", unit: "C" },
    "hum":    { name: "humidity",    unit: "%" },
    "kw":     { name: "power",       unit: "kW" },
    "status": { name: "state",       unit: null }
};

const parts = msg.topic.split("/");
// parts: [connectmq, demo, ab12cd, raw, deviceId, rawPoint]
const deviceId = parts[4];
const rawPoint = parts[5];

const loc = devices[deviceId];
const point = points[rawPoint];

if (!loc || !point || parts.length !== 6) {
    node.warn("Unmapped topic: " + msg.topic);
    return [null, msg];
}

// Accept plain numbers, strings or JSON objects with a "value" field
let value = msg.payload;
if (typeof value === "object" && value !== null && "value" in value) {
    value = value.value;
}
const numeric = Number(value);

const out = {
    topic: [PREFIX, loc.site, loc.area, loc.line, loc.machine, point.name].join("/"),
    payload: {
        value: isNaN(numeric) ? value : numeric,
        unit: point.unit,
        ts: new Date().toISOString(),
        source: msg.topic
    },
    retain: true,
    qos: 1
};

return [out, null];

Setting retain: true means any new subscriber immediately receives the last value of every data point, which is how most Unified Namespace consumers expect the tree to behave. The source field keeps a trail back to the original topic, which helps a lot when debugging.

Importable flow

You can import the complete flow with Menu → Import. Paste the function code above into the function node after import, and change the client ID and prefix to your own values.

[
  {"id":"b1","type":"mqtt-broker","name":"public broker","broker":"broker.connectmq.com","port":"1883","clientid":"nr-mapper-ab12cd","autoConnect":true,"usetls":false,"protocolVersion":"5","keepalive":"60","cleansession":true},
  {"id":"in1","type":"mqtt in","name":"raw devices","topic":"connectmq/demo/ab12cd/raw/#","qos":"1","datatype":"auto-detect","broker":"b1","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":100,"wires":[["fn1"]]},
  {"id":"fn1","type":"function","name":"map to ISA-95","func":"return [msg, null];","outputs":2,"x":340,"y":100,"wires":[["out1"],["dbg1"]]},
  {"id":"out1","type":"mqtt out","name":"publish ISA-95","topic":"","qos":"","retain":"","broker":"b1","x":560,"y":80,"wires":[]},
  {"id":"dbg1","type":"debug","name":"unmapped","active":true,"complete":"true","x":550,"y":140,"wires":[]}
]

The mqtt out node leaves qos and retain empty so the values set in the function node are used.

Testing the Mapping with mosquitto

Open a terminal and watch the structured side of the tree:

mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-ab12cd -v -t "connectmq/demo/ab12cd/izmir/#"

In a second terminal, simulate devices on the raw side:

mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-ab12cd -t "connectmq/demo/ab12cd/raw/th-0042/temp" -m "23.7"
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-ab12cd -t "connectmq/demo/ab12cd/raw/meter7/kw" -m '{"value": 4.2}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i pub-ab12cd -t "connectmq/demo/ab12cd/raw/unknown-1/temp" -m "19"

The first two messages should appear under izmir/packaging/line2/filler1/temperature and izmir/utilities/main/meter7/power. The third should show up in the Node-RED debug sidebar as unmapped.

Avoiding Loops and Other Pitfalls

The most common mistake in remapping flows is subscribing to a wildcard that also matches the output. If the mqtt in node listens on connectmq/demo/ab12cd/# and the flow publishes back under the same prefix, every republished message is received again, remapped again and eventually floods the broker. Keep raw and structured data in separate branches of the tree, and subscribe only to the raw branch.

With MQTT 5 you can also enable the no local option on the mqtt in node, so the client never receives messages it published itself. That is a safety net, not a substitute for good topic design, because a second Node-RED instance would still see the messages.

Other pitfalls worth checking:

Symptom Likely cause Fix
Nothing republished Function returns null on both outputs Check the debug output and the parts.length check
Output topic contains undefined Device in table but missing a level Validate each table entry has all four levels
Old values reappear after a rename Retained messages on the old topic Publish an empty retained payload to the old topic to clear it
Strings instead of numbers downstream Raw payload had units such as 23.7C Strip units in the function node before Number()
Disconnects on the public broker More than 10 messages per second per client Throttle with a delay node or use your own broker

Moving the Flow to Production

The public broker is fine for experiments, but production data belongs on a private broker with credentials and TLS. Create a broker at ConnectMQ, copy the generated username, password and client ID from the portal, and change the broker config node to:

  • Server broker.connectmq.com (or your broker host from the portal), port 8883, Use TLS enabled
  • Client ID YOUR_CLIENT_ID, username YOUR_USERNAME, password YOUR_PASSWORD
  • Protocol MQTT V5

The broker certificate is issued by Let's Encrypt, so the default trust store in Node-RED's Node.js runtime validates it without uploading a CA file. Our Node-RED MQTT over TLS guide walks through the TLS settings step by step.

Once the structured topics are flowing, the ConnectMQ portal detects the ISA-95 shaped namespace and shows your devices grouped by site, area, line and machine, and the live topic explorer lets you check the tree without opening a separate client. If some of your sources already speak Sparkplug B, you may not need Node-RED for them at all: the broker can decode Sparkplug B and republish it into ISA-95 topics, so Node-RED only has to handle the plain, flat topics. For PLC sources, the Node-RED Modbus guide shows how to read registers and feed them into the same mapping flow.

Because a managed broker handles the uptime, certificates and connection limits, the mapping flow becomes the only moving part you maintain. That is a large part of why teams move from a self-hosted Mosquitto box to a managed MQTT broker once the namespace grows.

Scaling the Mapping Table

When the number of devices grows past what you want to edit inside a function node, move the table out:

  • Retained configuration topic: publish the mapping as a retained JSON message, for example to config/topic-map, subscribe to it in a separate flow and store it in global.set("topicMap", msg.payload). The mapping function reads it with global.get("topicMap"). Updates take effect without a redeploy.
  • File or database: read a CSV or JSON file at startup with a file in node, or query a small table, which suits teams that already manage an asset register.
  • Pattern rules: if device IDs encode their location, such as iz-pk-l2-filler1-temp, a split-based rule can replace most of the table, with a small table for exceptions.

Whatever you choose, keep the unmapped output wired to something visible. New devices appear quietly, and an unmapped warning is often the only sign that a gateway has been replaced or renamed.

Run Your Node-RED Topic Mapping on ConnectMQ

A clean namespace needs a dependable broker underneath it. Sign up at app.connectmq.com for the free Serverless plan with no credit card, create a broker, and point your mapping flow at it with TLS on port 8883. You get MQTT 5, retained messages for the last value of every data point, and a devices view that understands your ISA-95 levels from the first message. For a broader view of hosting options, read our cloud MQTT broker guide.

Frequently Asked Questions

Can Node-RED rewrite topics without a function node?

Yes. A change node can set msg.topic with a JSONata expression, and that is enough for simple prefix swaps. A function node is easier to read once you need a lookup table, validation and payload normalisation in the same step.

Should I publish one topic per value or one JSON object per machine?

Both work in a Unified Namespace. One value per topic is simpler for dashboards and alert rules, while one object per machine reduces message count; pick one convention and apply it everywhere.

How do I stop the flow from republishing its own messages?

Subscribe only to a raw branch that the flow never writes to, and enable the MQTT 5 no local option on the mqtt in node as an extra safeguard.

Does ConnectMQ remap topics by itself?

It republishes Sparkplug B messages into ISA-95 topics, but it does not run arbitrary topic rewrite rules. For flat or vendor-specific topics, run the mapping in Node-RED or another MQTT client as shown here.

Related articles

Node-RED OPC UA Client to MQTT with ISA-95 Topics

Use node-red-contrib-opcua to subscribe to OPC UA variables on a PLC or simulation server, map node IDs to ISA-95 style MQTT topics, and publish values with quality and timestamps.

9 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.