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, multi-channel match (On channels OR-list), legacy On channel (name)/On channel #, 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 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.

  • On channels (multi-select) β€” pick one or more channels (unified by name across your sources). The trigger fires when a message arrives on any of the selected channels β€” an OR-list β€” so a single automation can cover "channel A or channel C" without a separate copy per channel. Leave none selected to match any channel. When set, this overrides the two single-channel fields below. Matching is by channel name and works for both Meshtastic and MeshCore messages.
  • On channel (name) β€” legacy single-channel name match (case-insensitive). Kept for backward compatibility with existing automations; the multi-select On channels above is preferred. Ignored when On channels is set.
  • On channel # β€” the raw slot index, still available for single-source cases. Ignored when On channels is set.

Saved automations that used the old single-channel fields keep working unchanged β€” no migration is needed.

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 (Meshtastic only β€” MeshCore sends always go to a channel/region, never a DM-by-node). {{ trigger.from }} replies to the sender.
  • Reply to the triggering message β€” on Meshtastic this threads the reply as a tapback (via the triggering packet id). MeshCore has no packet-id/thread concept on the wire, so on MeshCore this instead auto-prepends the sender mention @[<senderLabel>]: to the outgoing text β€” the same @[Name]: markup the in-app reply button uses β€” so an automation can reply to whoever sent the triggering message without hand-writing the mention. The label comes from {{ trigger.senderLabel }} (sender name β†’ channel name β†’ id), so even an anonymous channel post still gets a sensible mention; if nothing at all can be resolved nothing is prepended, and if your text already begins with an @[…] mention it is left as-is (no double mention). See Universal message tokens β€” reference the sender via {{ trigger.senderLabel }} / {{ trigger.fromName }}, not the raw {{ trigger.from }}.
  • MeshCore scope (advanced; MeshCore sources only β€” ignored by Meshtastic) β€” which region a MeshCore message floods to: Inherit (channel / source default), Match the triggering message's scope (reply on the same region it arrived on), Unscoped (flood, no region), or A specific region… β€” the latter reveals a Region picker (token-aware). See Regions / Scopes.

The overall send is a source Γ— channel matrix: each selected source posts to the matching local slot of each selected channel.

MeshCore channel-send auto-retry β€” because a MeshCore channel/broadcast send is an unacked flood, the Send message action (like every automated MeshCore channel sender) can optionally be resent once if no repeater is heard re-flooding it within 30 seconds. This is a global, opt-in, one-shot behavior (off by default) configured in Settings β†’ MeshCore Messaging; it never retries user-typed messages, never retries direct messages (those have their own always-on ACK retry), and the resend can never trigger a fresh automation. See Automated Channel-Send Auto-Retry.

Manage the node ​

Runs an admin/management operation on the subject node: Favorite / Unfavorite, Ignore / Unignore, or Delete.

Request data from a node ​

Asks a node to report data β€” the automation equivalent of the manual request buttons. Works on both Meshtastic and MeshCore sources.

  • Request β€” what to ask for: Telemetry, Position (Meshtastic), Traceroute / path, Node info exchange (Meshtastic), Neighbor info, or Announce self (advert).
  • Telemetry type β€” which metric set to ask for, when the request is Telemetry.
  • Via sources β€” which radio(s) to send the request through. Leave empty to use the triggering source β€” but a source is required for source-less triggers (Schedule / System).
  • Target node β€” node # (Meshtastic) or contact public key (MeshCore). Leave blank to target the triggering node. Not used for Announce self.
  • Channel # (advanced; Meshtastic only) β€” which channel to send the request on (e.g. a private sensor channel); ignored by MeshCore.

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.

Pause ​

Waits a number of seconds (0–300) before the next action in the branch runs β€” a bounded, in-process delay that serializes naturally with the sequential action executor, so later actions in the same branch wait for it. Use it to space out a sequence, e.g. Message trigger β†’ Pause β†’ Send a message to let a repeater finish transmitting before replying. The pause only lasts for this run and is not durable across a restart; the dry-run simulator resolves it instantly instead of actually waiting.

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

Universal message tokens (Meshtastic + MeshCore) ​

A message trigger (trigger.message) fires on both protocols, so these tokens are standardized to mean the same thing on each β€” use them and your automation is portable:

TokenMeshtasticMeshCore
{{ trigger.senderLabel }}Node long/short name, else !hex idParsed sender name, else channel name, else pubkey/channel-<idx>
{{ trigger.fromName }}Node long name β†’ short name β†’ idParsed sender name (from the Name: body prefix / resolved contact)
{{ trigger.channelName }}Channel name (empty on a DM)Channel name (empty on a DM / room post)
{{ trigger.isDM }} / {{ trigger.isChannel }}Direct message / channel broadcastDirect message / channel post
{{ trigger.protocol }}meshtasticmeshcore

{{ trigger.senderLabel }} is the "just works" label for addressing a reply β€” it always resolves to something usable. Prefer it (or {{ trigger.fromName }}) over the raw identity tokens:

  • {{ trigger.from }} / {{ trigger.fromId }} are raw identity: on Meshtastic the node number / !hex id; on MeshCore the sender's public key β€” or, for a channel message (which carries no per-sender key on the wire), the synthetic channel-<idx> slot key, not a sender identity.

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.

Live trace ("view logs") ​

Where the dry-run Test panel exercises a rule against a synthetic event, the live trace watches real events flowing through a rule without sending anything itself. Each rule in the Automations list has a Trace button that opens a live debug view of just that rule; once armed, every event that reaches the rule is streamed to the panel in real time (over the dashboard socket), showing why it did or didn't run:

  • fired β€” the trigger matched and the action steps ran; the per-step trace is shown.
  • prefiltered β€” the event was filtered out before the conditions ran (e.g. wrong source/channel), with the reason.
  • cooldown β€” the rule matched but was suppressed by its cooldown window.

The panel keeps the most recent entries in a rolling buffer and auto-stops after 5 minutes (and on close or disconnect), so a trace never runs unbounded.