Guides

Respond with a downlink

Answer a device in the same exchange that carried its uplink.

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

The device must be on a bidirectional contract
On Sigfox, a device type whose contract is not bidirectional cannot carry downlinks at all. The contract is fixed when the device type is created and cannot be changed afterwards. Check this before debugging anything else — no amount of correct configuration works around it.

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.

Constant hex
The same 8-byte answer every time. Right for an acknowledgement or a fixed command.
From a field
A dot-path into the message, e.g. 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:

NetworkWhere the acknowledgement comes fromResult
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 aboutAcknowledged on an ack; Failed, with the LNS's reason on the row, on a nack or a failure
SigfoxThe SERVICE_ACKNOWLEDGE callback. It names no frame, so it settles the device's rows sent in the last fifteen minutesAcknowledged 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!) }
Try it on your machine
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.

Keep the bridge fast
The device is holding its receive window open. A bridge sink that takes seconds to think will miss it. Set the timeout to something the device can actually wait for, and make sure the sink answers well inside it.

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:

  1. The contract is not bidirectional. Nothing else matters if this is wrong.
  2. The callback is not the bidirectional kind. A plain DATA_UPLINK has no reply channel.
  3. The rule did not match. Check the conditions, and whether an earlier rule with Stop ran first.
  4. 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).
  5. The codec has no encoder, or it returned null for these commands. The rule preview says which codecs have one; the API log says why nothing went.
  6. The bridge sink timed out, or answered without the named field.

Next

Filter with rules
Rule ordering, stop-on-match and the other actions.
Tune LoRaWAN receive windows
The RX1 delay that lets LoRaWAN answer in the same exchange.
When delivery fails
Diagnosing the outbound side.
Copyright © 2026