Sparkplug templates are the Sparkplug B way to define reusable equipment types, the MQTT equivalent of a user-defined type (UDT) in a PLC or SCADA system. In this guide you will learn how template definitions and instances work, how parameters, nesting and versions fit in, how to build a pump template in Python, and how to keep the same model consistent across many edge nodes.
Why Templates Exist
Without templates, a pump is just a set of loosely related metrics: Pump1/Speed, Pump1/Running, Pump1/Pressure. A second pump repeats the same names with a different prefix, and nothing tells the host application that both are pumps. If one edge node calls the metric Speed and another calls it RPM, every consumer has to know about the difference.
Templates fix this by separating the type from the instance. You define a Pump once with its member metrics, datatypes and default values. Each physical pump is then an instance that references the definition. A host application that understands templates can create matching objects automatically, apply the same displays and alarms to every pump, and detect when an instance does not match its type.
Definitions and Instances
Both definitions and instances are metrics with datatype 19 (Template) whose value is a Template message:
| Template field | Purpose | In a definition | In an instance |
|---|---|---|---|
| version | Optional version string | Recommended | Optional |
| metrics | Member metrics | Members with datatypes and defaults | Members with current values |
| parameters | Named, typed constants | Declared with defaults | Overridden per instance |
| template_ref | Name of the definition used | Not set | Required, points to the definition |
| is_definition | Marks a definition | true | false |
Where definitions live
Template definitions are published in the NBIRTH, before any instance that uses them. They are not repeated in data messages. Some platforms prefix definition names with a folder such as _types_/ to separate types from live tags; that is a platform convention rather than a requirement, so follow whatever your host application expects.
Where instances live
Instances can appear in the NBIRTH (node-level equipment) or a DBIRTH (equipment behind a device). Each instance carries the full set of member metrics with current values in the birth. Data messages then carry changes.
A Decoded Example
Here is a pump definition as it appears in an NBIRTH after decoding:
{
"name": "_types_/Pump",
"datatype": 19,
"templateValue": {
"version": "1.2",
"isDefinition": true,
"parameters": [
{ "name": "RatedSpeed", "type": 9, "floatValue": 1500.0 }
],
"metrics": [
{ "name": "Running", "datatype": 11, "booleanValue": false },
{ "name": "Speed", "datatype": 9, "floatValue": 0.0 },
{ "name": "DischargePressure", "datatype": 9, "floatValue": 0.0 },
{ "name": "StartCount", "datatype": 8, "longValue": "0" }
]
}
}
And an instance of it in a DBIRTH:
{
"name": "CoolingPump1",
"alias": 20,
"datatype": 19,
"templateValue": {
"templateRef": "_types_/Pump",
"version": "1.2",
"isDefinition": false,
"parameters": [
{ "name": "RatedSpeed", "type": 9, "floatValue": 2900.0 }
],
"metrics": [
{ "name": "Running", "datatype": 11, "booleanValue": true },
{ "name": "Speed", "datatype": 9, "floatValue": 2875.5 },
{ "name": "DischargePressure", "datatype": 9, "floatValue": 3.4 },
{ "name": "StartCount", "datatype": 8, "longValue": "1287" }
]
}
}
The instance overrides the RatedSpeed parameter because this pump has a different motor. Parameters are ideal for nameplate data and limits that differ per asset but do not change at runtime.
Build a Template in Python
This snippet builds the definition and an instance using classes generated from the Eclipse Tahu sparkplug_b.proto file. Our tutorial on how to decode Sparkplug B in Python shows the one-time code generation step.
import time
import sparkplug_b_pb2 as spb
BOOLEAN, FLOAT, UINT64, TEMPLATE = 11, 9, 8, 19
MEMBERS = [("Running", BOOLEAN), ("Speed", FLOAT),
("DischargePressure", FLOAT), ("StartCount", UINT64)]
def set_value(metric, datatype, value):
metric.datatype = datatype
if datatype == BOOLEAN:
metric.boolean_value = value
elif datatype == FLOAT:
metric.float_value = value
elif datatype == UINT64:
metric.long_value = value
def add_pump_definition(payload):
m = payload.metrics.add()
m.name = "_types_/Pump"
m.datatype = TEMPLATE
t = m.template_value
t.version = "1.2"
t.is_definition = True
p = t.parameters.add()
p.name, p.type, p.float_value = "RatedSpeed", FLOAT, 1500.0
for name, dtype in MEMBERS:
member = t.metrics.add()
member.name = name
set_value(member, dtype, False if dtype == BOOLEAN else 0)
def add_pump_instance(payload, name, alias, rated_speed, values):
m = payload.metrics.add()
m.name = name
m.alias = alias
m.timestamp = int(time.time() * 1000)
m.datatype = TEMPLATE
t = m.template_value
t.template_ref = "_types_/Pump"
t.version = "1.2"
t.is_definition = False
p = t.parameters.add()
p.name, p.type, p.float_value = "RatedSpeed", FLOAT, rated_speed
for name, dtype in MEMBERS:
member = t.metrics.add()
member.name = name
set_value(member, dtype, values[name])
nbirth = spb.Payload(timestamp=int(time.time() * 1000), seq=0)
add_pump_definition(nbirth)
dbirth = spb.Payload(timestamp=int(time.time() * 1000), seq=1)
add_pump_instance(dbirth, "CoolingPump1", 20, 2900.0,
{"Running": True, "Speed": 2875.5,
"DischargePressure": 3.4, "StartCount": 1287})
print(len(nbirth.SerializeToString()), "byte NBIRTH,",
len(dbirth.SerializeToString()), "byte DBIRTH")
In a real edge node, the NBIRTH also contains bdSeq and Node Control/Rebirth, as shown in Sparkplug birth and death certificates. Publish these payloads to spBv1.0/{group}/NBIRTH/{node} and spBv1.0/{group}/DBIRTH/{node}/{device}.
Updating Template Values at Runtime
Births carry complete instances, but data messages should stay small. Platforms differ in how they report changes to a template member. Some send the instance metric again in NDATA or DDATA containing only the members that changed. Others expose each member as its own aliased metric after the birth, so a speed change travels as a single scalar value. Both approaches rely on the birth to establish names, datatypes and aliases, which is why a host that misses the birth cannot interpret template updates either.
Before you commit to a design, publish a birth and a few data messages from your edge platform and look at the decoded result. Confirm that your host application updates the right instance member, that unchanged members keep their values, and that a rebirth restores the full instance. Doing this with two or three pumps in a test group takes minutes and avoids discovering a mismatch after hundreds of assets are configured.
Nesting and Composition
A member metric of a template can itself be a template. A Skid template might contain two Pump instances, a Tank instance and a few scalar metrics. Composition keeps models readable and lets you reuse small, well-tested types.
Keep nesting shallow. Every host must resolve each level, and deep hierarchies make births large. Large births matter on shared infrastructure: the public test broker accepts messages up to 64 KB, and each plan has its own message size limit. If a node models hundreds of assets, check the birth size early.
Keeping Templates Consistent Across Edge Nodes
Templates only pay off when every edge node uses the same definition. In practice, drift creeps in when nodes are configured by different people or updated at different times. These habits prevent it:
- Keep one central library of definitions under version control, and generate edge node configuration from it.
- Set the
versionfield and bump it on every change to members, datatypes or parameters. - Never change a member's datatype in place; add a new member or a new version.
- Use the same definition name everywhere, including case.
- Give per-asset differences to parameters, not to extra members.
- After deploying, compare the decoded NBIRTH definitions from every node against the library.
- Roll out new versions node by node and confirm the host accepts both during the transition.
Step 6 is where a decoded view helps. ConnectMQ decodes Sparkplug B in its portal topic explorer, so you can open each node's NBIRTH and compare definitions without writing a script. Note that it shows births when they are published; if a node is already online, request a rebirth to see them.
Common Problems
| Symptom | Likely cause | Fix |
|---|---|---|
| Host shows instance members as unknown | Definition missing from NBIRTH | Publish definitions in NBIRTH before instances |
| Instance rejected or partially displayed | template_ref does not match definition name | Exact name, case-sensitive |
| Same type behaves differently per node | Definitions drifted between nodes | Central library and version checks |
| Births too large | Many deeply nested instances | Flatten, split devices, or check plan limits |
| Some consumers ignore templates | Tool does not support datatype 19 | Use a host that supports templates, or flatten for that consumer |
Templates, ISA-95 and the Unified Namespace
Templates model what a piece of equipment is; ISA-95 models where it is. The two complement each other: a Pump type, instantiated under site, area and line. ConnectMQ can republish Sparkplug data into ISA-95 topics such as connectmq/factory1/production/line1/cnc1/telemetry, which gives non-Sparkplug consumers a readable location-based view. For more on structuring plant data, see MQTT, Sparkplug B and the Unified Namespace, the Sparkplug B data types reference, and the pillar guide to Industrial IoT with MQTT.
Get Started with ConnectMQ
Run your template-based edge nodes on a hosted MQTT broker without operating the infrastructure yourself, or prototype first on the public MQTT test broker with a random group ID. Create a free Serverless broker at app.connectmq.com, no credit card required, and inspect your template definitions and instances decoded in the portal.
Frequently Asked Questions
Are Sparkplug templates the same as PLC UDTs?
They serve the same purpose: a reusable type with named, typed members. Many edge platforms map PLC or SCADA UDTs directly to Sparkplug templates.
Where must template definitions be published?
In the NBIRTH, before any instance that references them. Instances can then appear in the NBIRTH or in DBIRTH messages.
What are template parameters used for?
Parameters hold per-instance constants such as rated speed, limits or nameplate data. The definition provides defaults and each instance can override them.
Do all Sparkplug host applications support templates?
No. Support varies, so check your host before modeling everything as templates. For consumers without support, flatten members into regular metrics or use a readable ISA-95 republished view.



