Making a Unified Namespace trustworthy — a three-part series: 1 · UNS vs Data Swamp · 2 · Data Contracts · 3 · Asset Models.
Part 1 made the case that a Unified Namespace without an enforced standard drifts into a data swamp, and that the cure is a standard you actually enforce. This post is about where that standard lives for the data moving across the namespace: in the contracts that govern what each message is allowed to say.
"The UNS doesn't define data governance." I read some version of that sentence in a LinkedIn comment, a conference Q&A, or a vendor deck about once a week, always delivered as a closing argument. It's true, and it's beside the point. The Unified Namespace is a blueprint built on a lean set of best practices — deliberately small, deliberately unfinished. The blueprint stops where your domain starts. Every team that has actually shipped one wrote data contracts on top. So instead of arguing whether the blueprint should do more, let's compare notes on what we build above it.
Two contracts, two jobs
Two distinct agreements have to be in place for a fleet of packaging lines to behave like a fleet. Most projects only nail down one of them, which is why we have a UNS so often turns into we have a broker.
A schematic contract is the agreement on shape. For a given message, what fields are present, what types do they have, which are required, how is it serialized. This is the layer JSON Schema, Avro, Protobuf, and Pydantic exist for. It keeps the wire safe — a producer can't silently rename a field, a consumer can't be handed garbage it has no way to reject.
A semantic contract is the agreement on meaning. What does it mean to be a packaging-line-state message? What real-world states is the line allowed to be in? Is the line running while the upstream buffer is empty and the machine is holding? Does an operator scanning a barcode count as a setup activity or its own event? When two sites publish a state field that both validate against the same schema, are they actually talking about the same thing?
Schematic contracts solve mechanical problems. Semantic contracts solve organizational ones — and they're the harder of the two, because the answer depends on agreements between people, not on a parser.
Neither layer subsumes the other. Trying to do both jobs with the same artefact is where most data-model conversations get stuck — usually as a JSON Schema with a 40-line description field that nobody reads.
The schematic layer: standardize the payload
Most Unified Namespaces are built on event-driven MQTT, and MQTT is deliberately indifferent to what you send through it. The broker routes a blob of bytes to a topic and never looks inside. That indifference is what makes it fast and decoupled — and it's also why, left alone, every producer invents its own payload shape. One line publishes 142, another {"value": "142"}, a third {"val": 142, "t": 1699...}. All three land happily in the namespace. Every consumer that wants to read them then needs bespoke, defensive parsing for each source, and any producer can rename a field tomorrow without a single thing stopping it.
The fix is a payload standard: a small set of payload types, each with a fixed shape, that everything publishes and everything reads. One of our workhorses is the metric — the canonical payload for a single value reported as it changes: the value itself, plus the metadata you almost always need around it, like the timestamp it was measured at. Once "a throughput reading" is always a metric with the same shape, any consumer can read any metric from any line without knowing who produced it or writing a parser for it. The shape is the contract.
How you enforce that shape in code is an implementation choice. franzmq is ours — the open-source library we use across our PREKIT projects. It's a thin layer over paho-mqtt: typed payloads as Python dataclasses, class-based topics that compose hierarchical paths, optional ISA-95 topic modeling, and a publish call that refuses to send anything not matching the declared payload type. A compliant publish looks like this:
from franzmq import Client, Topic, Metric
client = Client.autocreate_and_connect(client_id="line-7-edge")
topic = Topic(payload_type=Metric, context=("line-7", "throughput"))
client.publish(topic, Metric(value=142.0))
The payload shape is enforced by the Metric dataclass, the topic structure by the Topic class, and the version prefix lives in one place. There's no encode-it-yourself path an integrator can drift into — the easy way to publish is also the correct one.
The library is just one way to get there. Whatever you settle on — franzmq, an internal package, something off the shelf — the goal is the same: make a well-formed payload the path of least resistance. Once the wire is safe, the harder layer becomes tractable.
The semantic layer is where the work is
A typed Metric keeps the wire safe, but it doesn't tell you what a packaging line is. Metric(value="running") is well-formed and still meaningless: nothing says which values are allowed, what they mean, or whether two sites agree on them. That agreement is the semantic contract, and it has to be written down as code too.
The tempting move is to mint a new payload type and a new topic for every concept — a PackagingLineState message on its own .../packaging-line/state topic. We've learned not to. It fragments the namespace: every concept becomes a bespoke topic shape consumers must know about in advance, and discovery degrades into reading someone's topic list. So the meaning rides on the standard payload instead. Everything still flows as a Metric on the signal's standard topic; the semantic contract constrains what that metric's value is allowed to be, and the signal is tagged with the contract it implements:
from enum import StrEnum
from typing import ClassVar
class LineState(StrEnum):
RUNNING = "running"
IDLE = "idle"
STOPPED = "stopped"
FAULTED = "faulted"
CHANGEOVER = "changeover"
class LineStateContract:
"""Semantic contract: a line's state is one of LineState, carried as the
value of a standard Metric — not a new payload type, not a new topic."""
name: ClassVar[str] = "LineState"
expected_data_type: ClassVar[str] = "string"
A signal that carries line state declares implements_contract = "LineState". Its values then publish as ordinary metrics on the signal's ordinary topic — client.publish(signal.topic, Metric(value=LineState.IDLE)), where signal.topic is a normal alp/v1/_Metric/... address, not a special one minted for this concept. A publisher trying to send "runnnig" (typo), "production" (different vocabulary), or "running fast" (a richer state a consumer wasn't built for) is now writing against the contract, the enum, and the platform's validation — not against an open str.
And consumers never guess a topic from a concept name. They ask the platform which signals implement the contract — GET /signals/?implements_contract=LineState — and subscribe to the topics it returns. Meaning is pinned, the namespace stays uniform, and discovery is a query rather than tribal knowledge. (PREKIT ships contracts exactly this way — a liveness Heartbeat, a richer JSON EdgeExecutionContext for shopfloor events — and projects add their own.) When a contract needs to evolve, you change it in one place and every signal that implements it evolves with it.
The schematic part of this is reusable across customers. The semantic part almost always grows customer-specific extensions, because what counts as a packaging line — or a CNC cell, or a coating run, or a quality verdict — is partly universal and partly specific to a given operation. That's expected. The point isn't to make every customer's semantic contracts identical. It's to make sure they exist, in code, in the tooling, on the path of least friction.
Enforcement: code beats writing
Most data-model standards in industry live as PDFs. They sit in the project SharePoint, get cited in the kickoff meeting, and quietly stop being load-bearing from the second integration onward. Not because anyone is careless — because following a PDF requires effort while not following it is automatic. The path of least friction wins every time.
The standards that survive in production are the ones built into tools. The ones where using the standard is easier than not using it, because the library does the encoding, validation, and topic construction for you, and the alternative is hand-rolling all of it. A compliant publish is one line; a non-compliant one is a detour. Drift becomes the harder option.
That's the practical test for any data-model document you write: for the engineer who picks this up six months from now, is the path of least friction the path that follows the standard? If it isn't, people will find their way around the standard, when under pressure.
What contracts don't cover yet
Contracts govern what a message is allowed to say. But a message is published by something — a machine, a line, a cell — and the next question is what that thing is, and what claiming to be that kind of thing obligates it to expose. That's the asset model: the rung above message contracts, where a "machine" becomes a named type an asset declares and the platform validates. It's also where KPIs you can trust come from. That's Part 3.
Making a Unified Namespace trustworthy — a three-part series: 1 · UNS vs Data Swamp · 2 · Data Contracts · 3 · Asset Models.
For deeper background, see our earlier post on What Exactly Is a Unified Namespace? and on bridging the gap between ERP and the shopfloor.
