Respond with a downlink
Most traffic flows left to right. A downlink goes back: the device asks a question with its uplink, and the answer travels home in the reply to that same callback.
This is the one place the pipeline runs backwards, and it is time-critical — the device is waiting.
What has to be true first
You also need the uplink to be the kind that expects an answer. For Sigfox that
is a DATA_BIDIR callback.
Set up the rule
Flow → Rules → New rule. Match the uplinks that should be answered — usually
on messageType — and choose Respond with downlink.
Then pick where the answer comes from.
Where the answer comes from
The Center computes it
The Center returns a downlink you define: either a constant, or a value read from a field on the message.
decoded.cmdHex. Lets a decoder work out the
answer from what the device sent.Reading from a field is the more useful of the two: your decoder can inspect the uplink and produce the correct response, and the rule just forwards it.
The codec encodes commands
The third source turns the problem around. Instead of bytes, the rule carries commands — what the answer means:
[{ "name": "valveOpen", "args": { "value": false } }, { "name": "toggleProtocol" }]
and the device's codec turns them into the frame. A codec is the decoder you
already attached to the profile, with a second function beside decodeUplink:
function encodeDownlink(input: EncodeInput): EncodeOutput | null {
const frame = new Array(8).fill(0)
for (const c of input.commands) {
if (c.name === 'valveOpen') frame[0] = c.args?.value ? 0x01 : 0x02
if (c.name === 'toggleProtocol') frame[1] |= 0x01
}
return { bytes: frame }
}
Write it under Decoders → the codec → Encoder, or pick the codec in the
rule's Codec that encodes field and press Write the encoder — the
device's own codec is used unless the rule names one. It receives every command for
this reply, the uplink being answered (input.uplink, with what your decoder
returned as decoded), the radio and its byte limit, and returns one frame — or
null to send nothing this round. The Test with commands panel runs it on
the server exactly as a real reply would.
Why this is the one to reach for: the rule stays readable (when the meter is unpaid, close the valve), the byte layout lives in one place next to the decoder that understands the same device, and encoder rules compose — if two encoder rules match the same uplink, their commands land in the same frame. Other downlink rules keep first-match-wins; whichever downlink rule matches first decides which kind runs.
Queue a command for later
A rule answers with what it knows at that moment. Commands an operator wants to send later — "toggle the protocol next time this device listens" — go through the device's downlink queue: open the device page and press Queue a command on its Downlink queue card, or select several devices on the devices list and use Queue a command on the selection. A queued command has a name, optional JSON arguments, a priority, an optional expiry and a note.
Nothing in the queue is sent by itself. At the device's next reply opportunity,
an encoder rule with include queued commands on hands the waiting rows to
encodeDownlink beside its own commands (source: 'queue', with their id).
The frame goes out, and the rows it carried are marked Sent with the
message that carried them; what the encoder left out of applied stays
Queued for the next time. A command past its expiry is marked Expired
instead, and one you withdraw is Cancelled — the card keeps all of it as
history.
A Sent row does not stay there. The network reports what became of the frame, and the Center settles the rows it carried:
| Network | Where the acknowledgement comes from | Result |
|---|---|---|
| LoRaWAN (The Things Stack) | The LNS's own downlink_ack, downlink_nack and downlink_failed events — the project's webhook subscribes to them beside uplinks, and each names the frame it is about | Acknowledged on an ack; Failed, with the LNS's reason on the row, on a nack or a failure |
| Sigfox | The SERVICE_ACKNOWLEDGE callback. It names no frame, so it settles the device's rows sent in the last fifteen minutes | Acknowledged when downlinkAck is true, Failed otherwise |
On LoRaWAN only a confirmed frame makes the device acknowledge, so ask for one in the rule's downlink spec when you need the Acknowledged state.
Those LNS events also reach the pipeline as messages of their own — JOIN,
DOWNLINK_QUEUED, DOWNLINK_SENT, DOWNLINK_ACK, DOWNLINK_NACK,
DOWNLINK_FAILED — with no payload. They are never decoded; they flow through
rules and sinks the way a Sigfox service callback does, and you can pick them
out by message type in the pipeline filter and in Dead letters. None of
them is a reply opportunity: on LoRaWAN only an UPLINK can trigger a downlink
rule.
// Return only what fitted: the rest waits for the next opportunity.
return { bytes: frame, applied: input.commands.filter(c => c.source === 'queue' && used.has(c.name)).map(c => c.id!) }
npm run db:seed:example in api/ builds a complete example in the center
tenant — project Downlink example, a codec that decodes and encodes, a
device, the two rules above and one queued command. Put the mock source
(localhost:3003/ui) in Generic HTTP mode, point it at the printed ingest
URL and secret, and emit: the response carries the 8-byte answer, the mock
sink (localhost:3004/ui) shows the decoded delivery, and the device page's
Downlink queue card shows the command as sent.Bridge to a sink
The Center forwards the uplink to a sink you choose, waits for its JSON reply, and returns the hex from a named field in that reply.
This is for when your own system decides the answer — a server that knows the meter's schedule, or holds a pending command queue.
It comes with a timeout. If the sink does not answer in time, no downlink is returned and the device gets nothing.
The format
A Sigfox downlink is 16 hex characters — 8 bytes. Not more, not less.
Whether it is a constant, read from a field, built by the codec's encoder, or returned by a bridge sink, that is what has to come out. A field holding the wrong length is a common cause of "the rule matched but nothing arrived".
First match wins
If several rules could produce a downlink, the first one that matches provides it. Later downlink rules are ignored for that message.
So order downlink rules from most specific to most general, exactly as with any other rule.
Delivery still happens
Answering with a downlink does not stop the uplink being delivered to your sinks. The two are independent: the device gets its answer, and your systems still get the reading.
If you want a downlink instead of delivery, add a rule that drops the message after the downlink rule has run.
When nothing comes back
In order of likelihood:
- The contract is not bidirectional. Nothing else matters if this is wrong.
- The callback is not the bidirectional kind. A plain
DATA_UPLINKhas no reply channel. - The rule did not match. Check the conditions, and whether an earlier rule with Stop ran first.
- The value is not 16 hex characters. Check the field the rule reads, or what the encoder returns (the Test with commands panel shows the byte count).
- The codec has no encoder, or it returned
nullfor these commands. The rule preview says which codecs have one; the API log says why nothing went. - The bridge sink timed out, or answered without the named field.