Industrial IoT

Sparkplug Templates and UDTs Explained

Sparkplug templates bring PLC-style user-defined types to MQTT. This guide explains template definitions and instances, parameters, nesting and versioning, with Python code that builds a pump template and tips for keeping models consistent across edge nodes.

Sparkplug templates diagram showing one pump definition and several pump instances published by edge nodes
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

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:

  1. Keep one central library of definitions under version control, and generate edge node configuration from it.
  2. Set the version field and bump it on every change to members, datatypes or parameters.
  3. Never change a member's datatype in place; add a new member or a new version.
  4. Use the same definition name everywhere, including case.
  5. Give per-asset differences to parameters, not to extra members.
  6. After deploying, compare the decoded NBIRTH definitions from every node against the library.
  7. 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.

Related articles

IIoT Edge Gateway: What It Does and How to Choose One

An IIoT edge gateway connects machines that speak Modbus, OPC UA or proprietary protocols to MQTT and the cloud. Learn its core roles, common architectures and how to choose the right one.

7 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.