Macro examples — advanced
These recipes use variables (set with a little math), conditions (if / else), live
sensor reads (read_tag), and backend calls (call_backend). Author them on the Code tab —
the visual blocks editor is flat-only, so nested repeat/if show a "code view only" notice.
Start from the starter recipes if you haven't yet; the syntax primer there explains literals, parameters, and variables.
A macro runs once per trigger. A
read_tag+ifevaluates the sensor a single time and stops. To keep reacting (thermostat, hysteresis, watchdog), give the macro a schedule ("every 30 s") or fire it from a rule — see Trigger it automatically in Macros — program your devices.
A note on math: set takes either a value or one flat operation, e.g.
"value": { "a": { "var": "duty" }, "op": "+", "b": { "lit": 15 } } means duty = duty + 15.
Operators are + - * / % — no chaining, so build up step by step inside a loop.
PWM sunrise fade-in
For: easing a lamp from off to full over time — a wake-up light. Ramps duty 0 → 255 in steps.
Needs: a Level actuator (gpio-pwm).
{
"params": [
{ "name": "STEP_MS", "type": "number", "default": 2000, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "set", "var": "duty", "value": { "lit": 0 } },
{ "type": "repeat", "count": { "lit": 17 }, "body": [
{ "type": "pwm_write", "deviceRef": "<dimmerDeviceId>", "duty": { "var": "duty" } },
{ "type": "wait", "ms": { "param": "STEP_MS" } },
{ "type": "set", "var": "duty", "value": { "a": { "var": "duty" }, "op": "+", "b": { "lit": 15 } } }
] },
{ "type": "pwm_write", "deviceRef": "<dimmerDeviceId>", "duty": { "lit": 255 } }
]
}
PWM "breathing" effect
For: a soft ambient/status light that fades up and down repeatedly. One up-loop plus one
down-loop, wrapped in CYCLES.
Needs: a Level actuator (gpio-pwm).
{
"params": [
{ "name": "CYCLES", "type": "number", "default": 3, "min": 1, "max": 1000 },
{ "name": "BREATH_MS", "type": "number", "default": 60, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "set", "var": "duty", "value": { "lit": 0 } },
{ "type": "repeat", "count": { "param": "CYCLES" }, "body": [
{ "type": "repeat", "count": { "lit": 17 }, "body": [
{ "type": "pwm_write", "deviceRef": "<dimmerDeviceId>", "duty": { "var": "duty" } },
{ "type": "wait", "ms": { "param": "BREATH_MS" } },
{ "type": "set", "var": "duty", "value": { "a": { "var": "duty" }, "op": "+", "b": { "lit": 15 } } }
] },
{ "type": "repeat", "count": { "lit": 17 }, "body": [
{ "type": "set", "var": "duty", "value": { "a": { "var": "duty" }, "op": "-", "b": { "lit": 15 } } },
{ "type": "pwm_write", "deviceRef": "<dimmerDeviceId>", "duty": { "var": "duty" } },
{ "type": "wait", "ms": { "param": "BREATH_MS" } }
] }
] }
]
}
Smooth servo scan
For: stepping a servo across its range in small increments (a panning sensor, a slow sweep)
instead of jumping between two positions.
Needs: a Position actuator (gpio-servo, firmware 1.4.0+).
{
"params": [
{ "name": "MIN_US", "type": "number", "default": 1000, "min": 500, "max": 2500 },
{ "name": "STEP_US", "type": "number", "default": 50, "min": 1, "max": 500 },
{ "name": "STEPS", "type": "number", "default": 20, "min": 1, "max": 1000 },
{ "name": "SCAN_MS", "type": "number", "default": 100, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "set", "var": "pos", "value": { "param": "MIN_US" } },
{ "type": "repeat", "count": { "param": "STEPS" }, "body": [
{ "type": "servo_write", "deviceRef": "<servoDeviceId>", "us": { "var": "pos" } },
{ "type": "wait", "ms": { "param": "SCAN_MS" } },
{ "type": "set", "var": "pos", "value": { "a": { "var": "pos" }, "op": "+", "b": { "param": "STEP_US" } } }
] }
]
}
Scheduled thermostat
For: turning a fan on above a setpoint and off below it. Reads the temperature, decides, acts.
Needs: a temperature sensor and a Level actuator (gpio-pwm) fan, both
on the gateway. Give this macro a schedule (e.g. every 30 s) so it keeps regulating.
{
"params": [
{ "name": "SETPOINT", "type": "number", "default": 28, "min": -40, "max": 125 }
],
"blocks": [
{ "type": "read_tag", "deviceRef": "<tempSensorDeviceId>", "tag": "temperature", "var": "t" },
{ "type": "if", "left": { "var": "t" }, "op": "gt", "right": { "param": "SETPOINT" },
"then": [ { "type": "pwm_write", "deviceRef": "<fanDeviceId>", "duty": { "lit": 255 } } ],
"else": [ { "type": "pwm_write", "deviceRef": "<fanDeviceId>", "duty": { "lit": 0 } } ] }
]
}
Staged cooling (nested if / else)
For: three fan speeds by temperature band — off, half, full — using a nested if.
Needs: a temperature sensor and a Level actuator fan. Schedule it like the
thermostat above.
{
"params": [
{ "name": "HIGH", "type": "number", "default": 32, "min": -40, "max": 125 },
{ "name": "MID", "type": "number", "default": 26, "min": -40, "max": 125 }
],
"blocks": [
{ "type": "read_tag", "deviceRef": "<tempSensorDeviceId>", "tag": "temperature", "var": "t" },
{ "type": "if", "left": { "var": "t" }, "op": "gt", "right": { "param": "HIGH" },
"then": [ { "type": "pwm_write", "deviceRef": "<fanDeviceId>", "duty": { "lit": 255 } } ],
"else": [
{ "type": "if", "left": { "var": "t" }, "op": "gt", "right": { "param": "MID" },
"then": [ { "type": "pwm_write", "deviceRef": "<fanDeviceId>", "duty": { "lit": 128 } } ],
"else": [ { "type": "pwm_write", "deviceRef": "<fanDeviceId>", "duty": { "lit": 0 } } ] }
] }
]
}
Pump hysteresis (two thresholds)
For: filling a tank — start the pump below a low level, stop it above a high level, and do nothing
in between (the classic anti-chatter pattern). Two separate ifs.
Needs: a level sensor and an On/Off actuator pump. Schedule it.
{
"params": [
{ "name": "LOW", "type": "number", "default": 20, "min": 0, "max": 100 },
{ "name": "HIGH", "type": "number", "default": 80, "min": 0, "max": 100 }
],
"blocks": [
{ "type": "read_tag", "deviceRef": "<levelSensorDeviceId>", "tag": "level", "var": "l" },
{ "type": "if", "left": { "var": "l" }, "op": "lt", "right": { "param": "LOW" },
"then": [ { "type": "digital_write", "deviceRef": "<pumpDeviceId>", "value": { "lit": 1 } } ] },
{ "type": "if", "left": { "var": "l" }, "op": "gt", "right": { "param": "HIGH" },
"then": [ { "type": "digital_write", "deviceRef": "<pumpDeviceId>", "value": { "lit": 0 } } ] }
]
}
Step-and-shoot timelapse / indexing table
For: advance a stage, let it settle, fire a shutter (or trigger a station), wait the interval,
repeat — a motion timelapse or an indexing/dosing table. Nested loop + several device types.
Needs: a Move actuator (gpio-stepper) and an
On/Off actuator wired to the shutter/trigger.
{
"params": [
{ "name": "FRAMES", "type": "number", "default": 40, "min": 1, "max": 10000 },
{ "name": "STEP", "type": "number", "default": 200, "min": 0, "max": 1000000 },
{ "name": "SETTLE_MS", "type": "number", "default": 500, "min": 0, "max": 600000 },
{ "name": "INTERVAL_MS", "type": "number", "default": 3000, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "repeat", "count": { "param": "FRAMES" }, "body": [
{ "type": "move", "deviceRef": "<stepperDeviceId>", "steps": { "param": "STEP" }, "dir": { "lit": 1 }, "maxSpeedUs": { "lit": 400 }, "minSpeedUs": { "lit": 1200 }, "rampSteps": { "lit": 80 } },
{ "type": "wait", "ms": { "param": "SETTLE_MS" } },
{ "type": "digital_write", "deviceRef": "<shutterDeviceId>", "value": { "lit": 1 } },
{ "type": "wait", "ms": { "lit": 200 } },
{ "type": "digital_write", "deviceRef": "<shutterDeviceId>", "value": { "lit": 0 } },
{ "type": "wait", "ms": { "param": "INTERVAL_MS" } }
] }
]
}
Modbus soft-start setpoint ramp
For: ramping a Modbus register (a VFD speed reference, a heater setpoint) up gradually instead of
commanding the target in one jump.
Needs: an RS485/Modbus device. fc 6 writes one holding register at address.
{
"params": [
{ "name": "START", "type": "number", "default": 600, "min": 0, "max": 65535 },
{ "name": "INC", "type": "number", "default": 60, "min": 0, "max": 65535 },
{ "name": "STEPS", "type": "number", "default": 24, "min": 1, "max": 1000 },
{ "name": "RAMP_MS", "type": "number", "default": 500, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "set", "var": "sp", "value": { "param": "START" } },
{ "type": "repeat", "count": { "param": "STEPS" }, "body": [
{ "type": "modbus_write", "deviceRef": "<modbusDeviceId>", "fc": 6, "address": 100, "value": { "var": "sp" } },
{ "type": "wait", "ms": { "param": "RAMP_MS" } },
{ "type": "set", "var": "sp", "value": { "a": { "var": "sp" }, "op": "+", "b": { "param": "INC" } } }
] }
]
}
Precise dosing
For: dispensing a measured amount by pulsing a pump/valve a known number of equal shots. Needs: an On/Off actuator pump or valve.
{
"params": [
{ "name": "DOSES", "type": "number", "default": 10, "min": 1, "max": 10000 },
{ "name": "PULSE_MS", "type": "number", "default": 250, "min": 0, "max": 600000 },
{ "name": "GAP_MS", "type": "number", "default": 750, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "repeat", "count": { "param": "DOSES" }, "body": [
{ "type": "digital_write", "deviceRef": "<pumpDeviceId>", "value": { "lit": 1 } },
{ "type": "wait", "ms": { "param": "PULSE_MS" } },
{ "type": "digital_write", "deviceRef": "<pumpDeviceId>", "value": { "lit": 0 } },
{ "type": "wait", "ms": { "param": "GAP_MS" } }
] }
]
}
Out-of-range alert + webhook
For: raising an alarm and notifying an external system when a reading crosses a limit. emit
shows up in the run log / app; call_backend fires a saved action (a webhook or MQTT publish)
that runs on our servers.
Needs: a sensor and a saved Action (Rules → Actions) — paste its id into actionId.
{
"params": [
{ "name": "LIMIT", "type": "number", "default": 6, "min": 0, "max": 1000 }
],
"blocks": [
{ "type": "read_tag", "deviceRef": "<pressureSensorDeviceId>", "tag": "pressure", "var": "p" },
{ "type": "if", "left": { "var": "p" }, "op": "gt", "right": { "param": "LIMIT" },
"then": [
{ "type": "emit", "message": "Overpressure detected", "severity": "critical" },
{ "type": "call_backend", "actionId": "<actionId>" }
] }
]
}
Dead-sensor / no-flow watchdog
For: flagging a stuck reading — e.g. a flow sensor reporting exactly 0 while the pump should be
running — and calling out to an external system. Schedule it to run periodically.
Needs: a sensor and a saved Action (paste its id into actionId).
{
"params": [],
"blocks": [
{ "type": "read_tag", "deviceRef": "<flowSensorDeviceId>", "tag": "flow", "var": "f" },
{ "type": "if", "left": { "var": "f" }, "op": "eq", "right": { "lit": 0 },
"then": [
{ "type": "emit", "message": "No flow — check the sensor or pump", "severity": "error" },
{ "type": "call_backend", "actionId": "<actionId>" }
] }
]
}
Multi-pass ramped camera slider
For: the full camera slider — glide forward and back with smooth accel/decel, several passes,
logging each one. The µs-level timing and the ramp run on the gateway.
Needs: a Move actuator (gpio-stepper, firmware 1.2.0+). maxSpeedUs
is the fast end (smaller µs/step), minSpeedUs the slow ends of the ramp; rampSteps is how many
steps the accel/decel spans.
{
"params": [
{ "name": "DISTANCE", "type": "number", "default": 16400, "min": 0, "max": 1000000 },
{ "name": "MAX_SPEED_US", "type": "number", "default": 220, "min": 50, "max": 100000 },
{ "name": "MIN_SPEED_US", "type": "number", "default": 900, "min": 50, "max": 100000 },
{ "name": "RAMP_STEPS", "type": "number", "default": 700, "min": 0, "max": 100000 },
{ "name": "PASSES", "type": "number", "default": 3, "min": 1, "max": 1000 },
{ "name": "PAUSE_MS", "type": "number", "default": 300, "min": 0, "max": 600000 }
],
"blocks": [
{ "type": "repeat", "count": { "param": "PASSES" }, "body": [
{ "type": "move", "deviceRef": "<stepperDeviceId>", "steps": { "param": "DISTANCE" }, "dir": { "lit": 1 }, "maxSpeedUs": { "param": "MAX_SPEED_US" }, "minSpeedUs": { "param": "MIN_SPEED_US" }, "rampSteps": { "param": "RAMP_STEPS" } },
{ "type": "wait", "ms": { "param": "PAUSE_MS" } },
{ "type": "move", "deviceRef": "<stepperDeviceId>", "steps": { "param": "DISTANCE" }, "dir": { "lit": 0 }, "maxSpeedUs": { "param": "MAX_SPEED_US" }, "minSpeedUs": { "param": "MIN_SPEED_US" }, "rampSteps": { "param": "RAMP_STEPS" } },
{ "type": "wait", "ms": { "param": "PAUSE_MS" } },
{ "type": "emit", "message": "Pass complete", "severity": "info" }
] }
]
}
Staying inside the limits
Macros are bounded by design so a run can't get stuck:
- Up to 256 instructions after compiling (loops expand, so a big
repeatbody counts more). - A 10-minute total run cap — a longer run is aborted automatically.
- One macro runs per gateway at a time.
- Your plan may cap how many macros you can create and how many steps each can have — the Macros page shows your usage and any caps.
If a save is rejected for being too large, shorten a loop body or split the work into two macros.