Automation Engine β
New in 4.12
A generic, visual "when this happens, do that" builder β Home Assistant / Node-RED / IFTTT-inspired β that lets you create your own automations instead of relying on the hardcoded ones. It runs globally across every source, with optional per-source scoping.
The Automation Engine lives on its own top-level Automations tab. It complements the legacy Automation features (Auto Acknowledge, Auto Traceroute, Auto Ping, Auto Responder, Auto Announce, β¦): those remain available and unchanged, while the engine is the flexible "build it yourself" alternative. Where a legacy automation gives you one fixed form, the engine lets you wire a trigger β conditions β actions graph for almost any behavior you can describe.
Overview β
Each automation is a small graph built in a guided, linear builder:
WHEN β RULE (IF β¦ THEN β¦) β optional FINALLY (combine the rules' results)- WHEN β exactly one trigger that starts the automation (a message arrives, telemetry crosses a threshold, a schedule fires, β¦).
- RULE β one or more conditions that decide whether to act, each routing to its own actions (the IF/THEN). Conditions are routers with a true and a false path, so you build If / ElseIf / Else logic instead of the old fixed routing matrices.
- FINALLY (optional) β a combine step that runs its actions based on how the rules turned out: ANY, ALL, NONE, or ALWAYS (unconditionally).
Key properties:
- Global by design. An automation evaluates events from all connected sources at once (like Map Analysis), rather than being tied to a single radio. Use a Source filter condition (below) to scope a workflow to a subset of sources when you want.
- Permission-gated. The tab and its API are gated by a dedicated global
automationspermission, separate from the legacy per-sourceautomationpermission. - Cooldown / rate-limit per automation prevents mesh spam, plus a per-run action cap and a loop guard so an automation can't runaway-recurse.
- Variables β a separate management area for user-defined values (constants and runtime flags/counters) referenced anywhere as
{{ var.name }}. - Run log β every fire is recorded with its per-step outcome for debugging.
- JSON import/export β automations export to JSON (personal node ids are rewritten to portable system tokens). Imported automations always land disabled for review.
- Test / dry-run panel β preview an automation against a synthetic event with no mesh traffic, no notifications, and nothing saved.
Triggers β
Every automation has exactly one trigger (the WHEN). Each trigger exposes a set of {{ trigger.* }} fields you can use in conditions and message text (see Tokens).
| Trigger | Fires when⦠| Notable options |
|---|---|---|
| A message is received | A text/packet message arrives | Text contains (case-insensitive substring), Text matches regex, channel match, From node # |
| A new node is discovered | A node is seen for the first time | β |
| A node is updated | A node record changes (name, role, position, β¦) | β |
| Telemetry is received | A telemetry reading arrives | Metric filter (battery, voltage, temperature, channel utilization, air util TX, β¦) |
| On a schedule | A cron expression fires | 5-field cron expression |
| A system event | An engine/source lifecycle event | System start, Source came online, Source went offline, Upgrade available |
| A node enters/leaves a region | A node crosses a geofence | Enters / Leaves / Moves while inside (dwell), plus a map region editor |
Message trigger & channel-name matching β
The message trigger can filter on text (substring or regex) and on the channel. Prefer matching by channel name (On channel (name)) rather than raw slot index: the same logical channel can sit in a different slot on different sources, so a name match is portable across your whole mesh. The raw On channel # index is still available for single-source cases.
Schedule trigger (live cron) β
The schedule trigger fires on a standard 5-field cron expression (e.g. 0 * * * * = top of every hour). It is backed by a live croner job:
- A cron job is armed per enabled schedule automation; create / update / enable / disable / delete all re-arm correctly (the old job is stopped first, so there are never stale or duplicate jobs).
- The per-automation cooldown is honored on each fire.
- The cron is validated at save time (5-field, no seconds) β an invalid expression is rejected in the builder rather than silently never firing.
Because a schedule has no triggering message and no subject node, a Send a message action under a schedule trigger must name a target source (see Send a message).
System trigger β
Fires on engine/source lifecycle events: System start (MeshMonitor booted), Source came online, Source went offline, and Upgrade available (a new release was detected). The upgrade event exposes {{ trigger.latestVersion }} and {{ trigger.currentVersion }} for use in a notification.
Geofence trigger β
Defines a geographic region and fires when a node enters, leaves, or dwells (moves while inside) it. The region is drawn directly on a Leaflet map β either a circle (center + radius) or a polygon β using the shared geofence map editor. Evaluation is shape-aware (point-in-circle or polygon ray-cast). See also the dedicated Geofence Triggers page.
Conditions β
Conditions form the IF of each rule. Each condition is a router: matched events follow its true path to one set of actions, and non-matching events can follow a false path to a different set β this is how If / ElseIf / Else is built.
| Condition | What it checks |
|---|---|
| Always (no filtering) | A pass-through that always matches β use it when a rule should act unconditionally |
| Number comparison | A numeric field (==, !=, >, <, >=, <=). Fields come from the event (e.g. hop count, SNR/RSSI), the hydrated node record (battery, hops away, role, position, age, β¦), or the node's latest telemetry. The value can be a literal or {{ var.name }} |
| Text comparison | A string field (contains, equals, starts with, ends with, matches regex, doesn't contain) over message text, node name/role, etc. |
| Source is one ofβ¦ | The Source filter β restricts the workflow to a chosen subset of sources (the "global but scopeable" knob). Leave empty to allow any source |
| Distance from a point | The subject node is within / farther than N km of a reference lat/lon |
| Variable check | Compares a user-defined variable against a literal or another value; with no operator it tests "is set / flag raised?" |
| Time of day | The current time is within an HH:MMβHH:MM window |
A missing or undefined field never throws β numeric/string comparisons against it simply evaluate false.
FINALLY combine modes β
The optional FINALLY step runs its own actions based on the combined results of the preceding rules:
- ANY β at least one rule matched.
- ALL β every rule matched.
- NONE β no rule matched.
- ALWAYS β run unconditionally, regardless of the rules.
To make a rule contribute only its true/false result to a FINALLY combine (without doing anything itself), give it the Do nothing action (see below).
Actions β
Actions are the THEN. A rule's true path (and/or false path, and/or the FINALLY step) runs one or more actions.
Send a tapback (reaction) β
Reacts to the triggering message with an emoji. Minimal by design β it carries no routing logic (the conditions do the routing).
Send a message β
Sends text to a channel or as a DM, with full {{ }} token interpolation in the body.
- Send via sources β a multi-select of which radios to transmit through. MQTT sources are receive-only and excluded. Both Meshtastic and MeshCore sources are valid send targets. Leave it empty to use the source that triggered the automation β but a source is required for source-less triggers (System events and Schedules).
- On channels β a multi-select of channels, unified across sources by protocol + name and shown with MC / MT badges. The correct local slot is resolved per source, and a Meshtastic channel is never sent to a MeshCore source (and vice-versa). Disabled channel slots are excluded. Raw channel PSKs are never sent to the browser.
- DM to node # β send as a direct message instead of to a channel.
{{ trigger.from }}replies to the sender. - Reply to the triggering message β thread the reply to the message that fired the automation.
The overall send is a source Γ channel matrix: each selected source posts to the matching local slot of each selected channel.
Manage the node β
Runs an admin/management operation on the subject node: Favorite / Unfavorite, Ignore / Unignore, or Delete.
Send a notification (Apprise) β
Dispatches an out-of-band notification through Apprise with a Title, Body (both token-interpolated), and a Severity (Info / Success / Warning / Failure). It resolves the Apprise endpoint from the normal chain (per-source β global β APPRISE_URL β bundled service), and you can optionally supply inline Apprise URL(s) to override the target.
Run a script β
Runs a script file from the server's $DATA_DIR/scripts folder (the same directory the Auto Responder uses) when the automation fires.
- Script β picked from a dropdown of files in the scripts directory.
- The trigger context is passed to the script as
MM_*environment variables:MM_TRIGGER_TYPE,MM_SOURCE_ID,MM_NODE_NUM,MM_TIMESTAMP, and each trigger field asMM_<UPPER_SNAKE_NAME>(object values are JSON-stringified). Message-style aliases (MESSAGE,FROM_NODE, β¦) are provided for compatibility with existing scripts. - Store result in (optional) β captures the script's JSON stdout into a variable. Use a
jsontyped variable and index into the result later with{{ var.name.field }}(see Variables and Tokens). - A non-zero exit code is recorded as an action error on the run. Path-traversal protection, the interpreter pick, and the execution timeout are reused from the existing script runner.
The script itself does not send messages β capture its output into a variable, then use a separate Send a message action to relay it.
Set a variable / flag β
Writes a dynamic variable: Set to value, Increment by, Raise flag, or Clear / lower flag. Read-only constants can't be written here.
Do nothing β
A no-op action. Use it so a rule contributes only its true/false outcome to a FINALLY combine step without performing any action of its own.
Variables β
Variables are a separate, first-class management area under the Automations tab. A variable is referenced everywhere as {{ var.name }} and participates in conditions, actions, and text interpolation.
Two roles (a single readonly flag):
- Constant (
readonly) β you set the value directly in the Variables UI (e.g.lowBatteryThreshold = 20). Automations may read it but never write it. This is the "thresholds / config" case. - Dynamic β managed by automations at runtime via Set a variable / flag (flags, counters, last-seen values).
Types: string, integer, float, boolean, flag, and json.
- A
flagis a boolean that auto-clears after a configured duration. It's the anti-spam primitive: "have I already welcomed this node in the last 24 h?" β raise the flag when you act, and aVariable checkthat the flag is not set gates the next run. Expiry is evaluated at read time, so it survives restarts. - A
jsonvariable holds structured data β typically the captured output of a Run a script action β and is indexed with nested access (below).
Scopes decide what the value is keyed by:
| Scope | One value per⦠|
|---|---|
global | the whole instance |
source | source connection |
node | physical node (shared across sources) |
sourceNode | a (source, node) pair |
For scoped variables the key is resolved from the trigger context automatically β node / sourceNode bind to the trigger's subject node, source / sourceNode to the trigger's source. Schedule and system triggers have no subject node, so a node-scoped variable there needs an explicit reference.
Nested access: for json variables (and any object value), index into fields with {{ var.name.a.b }}. Referencing the whole variable renders it as JSON. Variable names must be dot-free identifiers so the name.path split is unambiguous.
Tokens β
Text fields that support substitution (message body, DM-to, notification title/body, condition values, the set-variable value) accept double-brace tokens:
| Token | Resolves to |
|---|---|
{{ trigger.* }} | A field from the current trigger (e.g. {{ trigger.text }}, {{ trigger.fromId }}, {{ trigger.hops }}, {{ trigger.value }}, {{ trigger.latestVersion }}). The available fields depend on the trigger type |
{{ trigger.sourceId }} / {{ trigger.timestamp }} | Available for every trigger; timestamp renders as a local date/time |
{{ var.name }} | A user-defined variable; {{ var.name.field }} for nested json access |
{{ NOW }} | The current time, rendered as a local YYYY-MM-DD HH:mm:ss |
In-builder validation β
Token-bearing fields render with live highlighting so typos surface immediately:
- A recognized token is shown blue.
- An unrecognized token (a typo like
{{ trigger.lastestVersion }}, or an unknown variable) is shown red with a wavy underline, and is also listed inline below the field ("Unrecognized token(s): β¦ β check for typos").
Recognition is built from the trigger's token set plus your known variable names. It's a non-blocking hint β it won't stop you saving, so a valid-but-unenumerated token is never falsely rejected.
Substitutions help drawer β
A ? button at the top of the builder opens a docked, non-modal Substitutions sidebar that stays open while you edit. It lists every {{ trigger.* }} token for the current trigger type (and the rest), plus {{ var.* }} and {{ NOW }}, so you can author tokens without leaving the field.
Testing (dry-run) β
The builder includes a βΆ Test panel that runs the automation against a synthetic event with no mesh IO, no Apprise dispatch, and nothing persisted. It returns the full trace β whether the trigger matched, each condition's verdict, the resolved action parameters, and any simulated variable writes. A Run a script action is stubbed in the dry-run, so testing never spawns a process.
You supply the synthetic inputs the conditions need:
- Message inputs β text, plus SNR, RSSI, and a Via MQTT toggle (so
{{ trigger.snr }}/{{ trigger.rssi }}can be exercised, including the MQTT case where signal metrics are absent). - Subject-node facts β Hops away, channel utilization, air-util TX, node SNR, altitude, and more, so
node.*conditions can actually be made true. - System Event and telemetry Metric are dropdowns (not free text, which would silently no-match on a typo); a From source selector lets you exercise the Source filter condition; and a schedule trigger dry-runs as matched.
The result is rendered human-readably β the interpolated message text, the tapback emoji, the notification title/body/URLs β with the raw resolved parameters behind a toggle. When a run matches the trigger but no action fires, the panel explains that every condition went false and points at which inputs/facts to change.