A Unified Namespace (UNS) is a single, event-driven place where every system in a company publishes its current state, organised by where things are in the business rather than by which software produced them. In practice it is an MQTT broker plus a topic hierarchy — usually shaped after ISA-95: enterprise / site / area / line / cell — with retained messages holding the latest value of everything.
This tutorial shows how to design that hierarchy, decide what to retain, choose payload conventions, fit Sparkplug B in, and test the namespace with a few small scripts.
Why a UNS instead of point-to-point integrations
Classic plants integrate system by system: the MES polls the SCADA, the ERP imports a CSV from the MES, a dashboard queries a historian. Every new consumer means a new integration, and nobody has one place to see the present state of the operation.
A UNS replaces this with a hub:
| Point-to-point | Unified Namespace | |
|---|---|---|
| Integrations | One per pair of systems | One per system (to the broker) |
| Adding a consumer | New interface on the source | Subscribe to a topic |
| Current state | Spread across databases | Retained on the broker |
| Data flow | Polling, batch | Event-driven, report by exception |
| Ownership | Each interface owned separately | One namespace, governed centrally |
Producers — PLC gateways, MES, quality systems, ERP adapters — publish into their branch of the tree. Consumers subscribe to whatever they need. Nobody needs to know who else is connected.
Design the ISA-95 hierarchy
ISA-95 gives you a vocabulary for the physical and organisational structure of a plant. Mapping it directly to topic levels makes the namespace readable for both OT and IT people:
| Level | Example | Topic level |
|---|---|---|
| Enterprise | Acme Foods | acme |
| Site | Dallas plant | dallas |
| Area | Packaging | packaging |
| Line (work center) | Line 1 | line1 |
| Cell (work unit) | Filler | filler |
| Data | Speed, state, counts | speed, state, good-count |
So the filler's speed lives at acme/dallas/packaging/line1/filler/speed. Below the ISA-95 levels, add a small set of namespaces by data kind so consumers can tell state from events:
acme/dallas/packaging/line1/filler/state "running" retained
acme/dallas/packaging/line1/filler/speed {"v":118,...} retained
acme/dallas/packaging/line1/filler/good-count {"v":48211,...} retained
acme/dallas/packaging/line1/filler/events/fault {"code":"E14"} not retained
acme/dallas/packaging/line1/mes/work-order {"id":"WO-881"} retained
acme/dallas/packaging/line1/quality/last-check {"result":"ok"} retained
acme/dallas/packaging/line1/filler/cmd/setpoint (restricted) not retainedRules that keep a namespace healthy over the years:
- Lower-case, no spaces, ASCII only, hyphens inside a level. Topics are case-sensitive, and mixed case guarantees duplicates.
- Name by place, not by source.
line1/filler/speed, neverkepware3/channel2/tag17. Sources change; the line stays. - Keep levels fixed in depth inside a branch, so
acme/+/packaging/+/filler/stateworks across all sites. - Separate commands under
cmd/and protect them with ACLs; most producers should never be able to write there. - Do not put values in topic names (no
.../temp/21.5); the value belongs in the payload.
The general principles are covered in topic design best practices. Before committing to a layout, try your subscription patterns in the topic matcher.
Retained state is the heart of a UNS
The namespace should answer "what is happening right now?" to a client that connected one second ago. That is exactly what retained messages do: the broker stores the last retained message per topic and delivers it to every new subscriber immediately.
- Retain state: values, statuses, current work order, last quality result, setpoints.
- Do not retain events: faults raised, parts rejected, commands. A new subscriber should not see yesterday's fault as if it just happened.
- Clear obsolete topics by publishing an empty retained payload when a machine is decommissioned, or the namespace slowly fills with ghosts.
- Use MQTT 5 message expiry on state that becomes meaningless if not refreshed, so stale values disappear instead of lying.
Pair every producer with a retained status topic and a Last Will (offline), as shown in the Modbus gateway tutorial. Consumers can then tell a stable value from a dead source.
Payload conventions
Agree on one payload shape for state and stick to it. A simple, self-describing JSON works well:
{ "v": 118, "unit": "bpm", "ts": "2026-10-17T08:41:07.214Z", "q": "good" }v is the value, unit avoids guessing, ts is the source timestamp in UTC, q the quality. Static metadata (descriptions, ranges, engineering units) can live in a retained _meta sibling so the hot path stays small.
Seed and browse a test namespace
Two small paho-mqtt 2.x scripts let you experiment before any real producer exists. The first publishes a retained snapshot of a tiny plant with a one-hour expiry:
import json, time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
ROOT = "testmqtt/uns/acme/dallas/packaging/line1"
STATE = {
"filler/state": "running",
"filler/speed": {"v": 118, "unit": "bpm"},
"filler/good-count": {"v": 48211, "unit": "pcs"},
"capper/state": "idle",
"mes/work-order": {"id": "WO-881", "product": "SKU-330ml"},
}
props = Properties(PacketTypes.PUBLISH)
props.MessageExpiryInterval = 3600 # seconds
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
c.connect("broker.connectmq.com", 1883)
c.loop_start()
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
for path, value in STATE.items():
body = value if isinstance(value, str) else json.dumps({**value, "ts": now})
c.publish(f"{ROOT}/{path}", body, qos=1, retain=True, properties=props).wait_for_publish()
c.loop_stop()
c.disconnect()
print("seeded", len(STATE), "topics")The second connects as a brand-new client, collects whatever the broker hands it for a few seconds and prints the namespace as a tree — exactly what a new consumer would see:
import time
import paho.mqtt.client as mqtt
FILTER = "testmqtt/uns/acme/#"
seen = {}
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(FILTER, qos=1)
def on_message(client, userdata, msg):
seen[msg.topic] = (msg.payload.decode(errors="replace"), msg.retain)
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
c.on_connect, c.on_message = on_connect, on_message
c.connect("broker.connectmq.com", 1883)
c.loop_start()
time.sleep(3)
c.loop_stop()
for topic in sorted(seen):
payload, retained = seen[topic]
depth = topic.count("/") - FILTER.count("/") + 1
print(" " * depth + topic.rsplit("/", 1)[1], "=", payload, "(retained)" if retained else "")If the browser prints every seeded topic marked (retained), your state layer works. You can do the same check visually in the online MQTT client:
Browse the test namespace in your browser
The shared endpoint is fine for trying the layout. A real namespace holds the live state of your operation, so run it on a broker you control.
Where Sparkplug B fits
Sparkplug B is a specification on top of MQTT for industrial devices: a fixed topic structure (spBv1.0/<group>/<message type>/<edge node>/<device>), Protobuf payloads, birth/death certificates and a primary host that tracks state. It solves the edge problem very well, but its topics are shaped by message type and edge node, not by ISA-95.
| Approach | How it works | Good when |
|---|---|---|
| Plain MQTT UNS | Producers publish JSON straight into ISA-95 topics | Mixed IT/OT consumers, simple tooling, readable topics |
| Sparkplug at the edge, UNS above | Edge nodes speak Sparkplug; a host or bridge republishes into ISA-95 topics | Many devices, need for standard state and compact payloads |
| Sparkplug only | Consumers all understand Sparkplug | Homogeneous SCADA ecosystem |
The hybrid model is popular for a reason: devices get Sparkplug's state management, and the rest of the company gets a readable namespace. The trade-offs are discussed in Sparkplug vs plain MQTT. Data from OPC UA servers can enter the same tree through an OPC UA to MQTT bridge.
A testing checklist for your namespace
- Fresh-subscriber test: a new client subscribing to
acme/<site>/#receives the current state of every asset immediately (all retained). - Event test: events arrive live but are not replayed to a new subscriber.
- Source-swap test: replace a producer (simulated gateway → real gateway); consumers notice nothing.
- Dead-source test: kill a producer; its
statusflips toofflinevia the Last Will. - ACL test: try to publish into another line's branch with a gateway's credentials — it must be refused.
- Wildcard test: every dashboard filter matches exactly the intended topics in the topic matcher.
A Unified Namespace is less about technology than about agreement: one broker, one hierarchy everyone understands, retained state for the present and clear rules about who publishes where. Start with one line, seed it, browse it, break it on purpose — and grow the tree only when the first branch has proven itself.
Frequently asked questions
Is a Unified Namespace a product?
No. A UNS is an architecture pattern: one event-driven hub, usually an MQTT broker, where every system publishes its current state into a shared, business-shaped topic hierarchy. You build it from a broker, naming rules and the clients that publish and consume.
Do I need Sparkplug B for a Unified Namespace?
No. Many UNS implementations use plain MQTT with JSON payloads and ISA-95 shaped topics. Sparkplug B is useful at the edge for standard birth/death state and compact payloads; a common pattern is Sparkplug from devices to a host, which then republishes into the ISA-95 namespace.
Should every message in the UNS be retained?
Retain current state (values, statuses, setpoints) so any new consumer sees the present immediately. Do not retain events such as alarms raised, parts produced or commands; those belong to the moment they happen and to a historian afterwards.

