Decode the payload
A device sends 00018d460000300040e12402. Your billing system wants
index: 18.061. A decoder is what happens in between.
Decoding is worth doing early: rules filter on decoded fields, and the custom output editor can only offer you fields it has seen a decoder produce.
Where a decoder lives
A decoder attaches to a device profile's technology, not to a device. Define it once for the model and every device on that profile decodes the same way.
Where: Profiles → Decoders to write one, then attach it on the profile's technology. Or Flow → Decoders, which is the same thing from the pipeline view.
Pick a kind
Prefer grammar when it fits. It cannot loop forever, cannot throw in surprising ways, and reads as documentation of the payload format.
The two output groups
A decoder returns readings in two groups, and the distinction carries all the way to your sink:
{
"telemetry": { "index": 18.061, "unit": "m3", "battery": 100 },
"meta": { "seqNumber": 18, "rssi": -103.41 }
}
telemetry is what the device measured. meta is about the transmission.
Put things in the right group — some output shapes deliver only one of them, and
a reading filed under meta will go missing from a sink expecting telemetry.
The JavaScript sandbox
JavaScript decoders run in an isolated V8 sandbox:
- No network, no filesystem. A decoder cannot call out.
- Memory and time limited. A runaway script fails that one message; the pipeline keeps running.
- TypeScript syntax is accepted; types are stripped before execution.
When decoding fails
Decoding is best-effort. A decoder that throws, a profile with no decoder, or a callback carrying no payload at all does not fail the message — it continues with whatever it had.
That is deliberate: losing a reading is worse than delivering it undecoded. But it means a sink can see two different shapes for the same device.
Check payload.decodeError on a message that looks wrong. It is the
difference between "the decoder is broken" and "this callback never had a payload
to decode".
DATA_UPLINK, DATA_BIDIR and SERVICE_DATA_ADVANCED contain a payload
frame. Status, acknowledge, repeater and error callbacks have nothing to decode
and arrive as they came — that is not a decoder fault. Filter them out with a
rule if your sink should not see them.Decryption
If the profile defines encryption, the payload is decrypted before decoding, using the key on the device's binding. Keys are encrypted at rest and never shown again — leave the field blank when editing to keep the stored value.
Test it
The editor has a Test with a payload panel. Paste the bytes a device would
send as hex, set the message type and any other input fields the callback
would carry (device, seqNumber, rssi, ack…), and press Run. The
script runs on the server through the same codec ingest uses — never a copy in
the browser — so what comes back is what a real message would produce:
telemetry, meta, the error if it threw, and the list of fields Routing
would offer.
The most common surprise shows up here in one click: a payload of the wrong
length. A decoder that checks bytes.length and returns only
meta.error produces no telemetry, and a sink wired to it receives only that
error — exactly what the panel shows before you find out from the sink.
Use as routing sample keeps the run as the decoder's recorded output shape and sample. That is the same record ingest writes after the first real message, written before it: open Flow → Routing for a pair using this source and the field picker lists these fields, the preview shows these values — rendered by the code that performs delivery, so what you see is what the sink receives. If the script changed since it was saved, the button saves it first; the sample must come from the script that will actually run.
A real uplink later refreshes the record with what the device actually sent.
The other direction: encode a downlink
A decoder is one half of a codec. The same row can carry a second function,
encodeDownlink, for when the Center has to answer the device. It receives
commands — { name: 'valveOpen', args: { value: false } } — from a rule
(and later from the device's queue), plus the uplink being answered, and returns
the one frame to send: { bytes } or { hex }, exactly 8 bytes on Sigfox,
whole bytes on LoRaWAN.
Open the codec, switch Decoder → Encoder, turn on This codec has an encoder, and write it against the ambient types the editor loads. The Test with commands panel runs it on the server with the commands and uplink you describe and shows the frame byte by byte. The rule that uses it is described in Respond with a downlink.