Skip to content

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 automations permission, separate from the legacy per-source automation permission.
  • 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).

TriggerFires when…Notable options
A message is receivedA text/packet message arrivesText contains (case-insensitive substring), Text matches regex, channel match, From node #
A new node is discoveredA node is seen for the first timeβ€”
A node is updatedA node record changes (name, role, position, …)β€”
Telemetry is receivedA telemetry reading arrivesMetric filter (battery, voltage, temperature, channel utilization, air util TX, …)
On a scheduleA cron expression fires5-field cron expression
A system eventAn engine/source lifecycle eventSystem start, Source came online, Source went offline, Upgrade available
A node enters/leaves a regionA node crosses a geofenceEnters / 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.

ConditionWhat it checks
Always (no filtering)A pass-through that always matches β€” use it when a rule should act unconditionally
Number comparisonA 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 comparisonA 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 pointThe subject node is within / farther than N km of a reference lat/lon
Variable checkCompares a user-defined variable against a literal or another value; with no operator it tests "is set / flag raised?"
Time of dayThe 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 as MM_<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 json typed 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 flag is 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 a Variable check that the flag is not set gates the next run. Expiry is evaluated at read time, so it survives restarts.
  • A json variable 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:

ScopeOne value per…
globalthe whole instance
sourcesource connection
nodephysical node (shared across sources)
sourceNodea (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:

TokenResolves 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.