Speak the same binary schema as your gateway
Industrial gateways and PLCs often publish binary protobuf on MQTT while the broker maps plant tags (Modbus, S7, OPC UA) to plain topics. LoT models let you decode those commands, run normal tag logic in the middle, and encode telemetry back — without a separate transformation service.Dynamic protobuf encoding and decoding in LoT is beta. Prefer
PUBLISH MODEL for binary MQTT output. Match your vendor .proto with PROTO_TAG and report issues with that layout and your broker version.When to use this
Use protobuf models when:- a gateway or SCADA system publishes raw protobuf bytes on MQTT;
- you must emit protobuf that a vendor decoder already understands;
- field numbers in the
.protoare sparse (for example101) or nested; - a gRPC connector should forward those bytes to a gRPC service. The connector does not decode protobuf.
GET JSON when payloads are JSON. Use the Sparkplug B Connector when the schema is Sparkplug B (spBv1.0/#), not a custom plant .proto.
Start from the direction you need: decode inbound bytes, publish outbound bytes, or both.
Quick Start
- Decode a command
- Publish telemetry
- Nested float fields
Read top-level fields from a binary command payload with The Action reads
GET PROTO. Declare every command arm so unused submessages keep their wire numbers:command_type from the triggering payload:Where protobuf applies
Define protobuf models for edge topics. Use
GET PROTO to read and PUBLISH MODEL to write. Tag reads and writes stay on your existing OT connectors.
Match your .proto
To stay aligned with a hand-written schema (including non-contiguous field numbers and nested messages):
PROTO_TAG— wire numbers must match the.proto(not field names).- Nested messages you fill on publish —
ADD MODEL ChildName "field"whereChildNameis aDEFINE MODEL. - Reserved wire slots —
ADD OBJECT "field" PROTO_TAG nwhen the parent must keep empty submessage fields so later tags stay aligned.
GET PROTO and GET PROTOBUF are the same verb. PROTO and PROTOBUF are the same format keyword.
Type mapping
A field is only decoded when the wire type the sender used matches the type your model implies. Get this wrong and that field is skipped — there is no error on the wire.ADD FLOAT is protobuf float — not a nested model named FLOAT. FLOAT and SINGLE are reserved type keywords; outside protobuf they behave as aliases of DOUBLE.
After a float is decoded it becomes a LoT number, so AS DOUBLE on a FLOAT field is valid. LoT has no separate 32-bit numeric type.
ADD INT is a varint. Proto fixed32, sfixed32, fixed64, and sfixed64 use a different wire encoding and have no LoT keyword (ADD FIXED32 does not exist). If those types appear in the vendor schema, declaring them as ADD INT will skip or misread the field.
What happens on a mismatch
The decoder follows the protobuf unknown-field rule rather than failing the whole message:
A skipped field is absent, and
GET PROTO returns null for it. There is no error log — an unexpected null almost always means a wire-type mismatch.
How field numbers are assigned
The broker does not ship a plant schema. Numbers come from yourDEFINE MODEL:
If the
.proto uses field 101 but LoT omits PROTO_TAG 101, the next sequential number is used and the gateway decoder fails.
Inbound: GET PROTO
Prefer IN PAYLOAD on the triggering topic. Use the other sources when the bytes are already cached or wrapped as Base64:
Pass
USING MODEL "Name" unless a DEFINE MODEL … WITH TOPIC already registered that schema on the topic. AS INT / DOUBLE / FLOAT / SINGLE / STRING / BOOL / BYTES selects the LoT type after decode. On a nested path, USING MODEL still names the top-level message.
Read from the last payload stored on another topic:
- Each
GET PROTOdecodes the payload from scratch. Reading two fields means two independent passes. USING MODELalways names the top-level message, even when the path is nested.- A mismatch inside the child does not fail the parent. Sibling fields still decode.
ADD FLOATis what makes a 32-bit field readable. Declaring itADD DOUBLEmakes the decoder expect 8 bytes where the sender wrote 4 — the field is skipped.
IF PAYLOAD IS PROTO to detect binary MQTT. Call GET PROTO … IN PAYLOAD instead.
Parent command with reserved field numbers
Vendor.proto files often declare several command arms but send only one. List every submessage slot on the parent so unused fields do not shift wire numbers. Use ADD OBJECT for unused slots. When publishing a filled nested message, use ADD MODEL on the parent instead.
ADD OBJECT holds a wire slot open but carries no schema, so a dotted path through an ADD OBJECT field cannot resolve. Use ADD MODEL on the arm you read.
Middle: tags stay JSON
Between command and telemetry, most OT setups already have tag values on topics — from aDEFINE CONNECTOR mapping, a collapsed JSON model, or single-value publishes. That step stays JSON or plain text unless your plant standard requires protobuf on those topics too.
Outbound: PUBLISH MODEL
Topic vs format
The next
PUBLISH MODEL to the same topic merges: binary for PROTO only; JSON from the main topic for JSON / BOTH. KEEP + protobuf updates the byte cache without per-field JSON fan-out.
Trigger-based models (WITH TOPIC + AS TRIGGER) still auto-publish JSON on the base topic. Use PUBLISH MODEL when the edge must see binary.
Response envelope with sparse tags
Alarms and events often use high field numbers (101, 102). Tag them explicitly; useADD MODEL for nested bodies you fill on publish. MessageHeader and ProcessValues match Quick Start — the new pieces are AlarmEvent and field 101:
PUBLISH MODEL / GET PROTO. Wire numbers come only from PROTO_TAG. Set message_type to the enum integer your .proto expects.
Raw bytes fields
Some messages carry opaque bytes (recipe blob, certificate, file chunk). Assign them with BYTES FROM BASE64:
payload from a Base64 JSON field:
PAYLOAD AS BYTES instead of GET PROTO.
Common pitfalls
ADD OBJECT vs ADD MODEL
ADD OBJECT vs ADD MODEL
ADD OBJECT when publishing a populated nested message produces JSON-in-bytes. Use ADD MODEL.Missing PROTO_TAG on sparse fields
Missing PROTO_TAG on sparse fields
Events at 101+ need an explicit tag. Omitting unused command slots on the parent also shifts later wire indexes.
GET PROTO needs ADD MODEL
GET PROTO needs ADD MODEL
Dotted
GET PROTO paths require the parent to declare the child with ADD MODEL.ADD DOUBLE on proto float
ADD DOUBLE on proto float
The field is skipped. Use
ADD FLOAT (or ADD SINGLE).USING MODEL names the top
USING MODEL names the top
On a nested read, name the top-level message, not the child model.
Reserved words as variable names
Reserved words as variable names
A variable may use a name shaped like a LoT keyword (
store, unit, limit, device, and the rest). Inside {…} — expressions, conditions, topic strings, payloads, and PUBLISH MODEL … WITH field values — that name is a variable reference. Keyword matching is case-insensitive, so {Device} and {device} are the same variable.A protobuf field name is not that reference. The model can still declare a field called device; {device} reads the variable, not the field label.Troubleshooting
GET PROTO returns "null" rather than raising, so a wrong schema looks like missing data.
To confirm what is on the wire, decode a captured payload with a protobuf raw decoder (
protoc --decode_raw). It reports field numbers and wire types without needing the schema — that pair must match your DEFINE MODEL.
Next Steps
Publishing Models
Control when and where COLLAPSED models publish, including protobuf bytes.
GET PROTO in Actions
Full GET PROTO syntax, sources, and types.

