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.
GET JSON when payloads are JSON. Use the Sparkplug B Route 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
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 routes.
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 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 / STRING / BOOL / BYTES selects the LoT type after decode.
Read from the last payload stored on another topic:
- Each
GET PROTOdecodes the payload from scratch. Reading two fields means two independent passes.
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.
Middle: tags stay JSON
Between command and telemetry, most OT setups already have tag values on topics — from aDEFINE ROUTE 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.
Reserved words as variable names
Reserved words as variable names
device is a LoT keyword, so SET "device" may parse but interpolating that name does not. Use device_id. If an Action fails with Unexpected '<name>' where IDENTIFIER was expected, rename the variable.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.

