#Rules Engine
Both Edge Hub ingestion services — tsend2mqtt (PLC)
and iolinkmaster2mqtt (IO-Link) — embed the same
rules engine and use the same XML schema. This page is the reference for
that schema.
The engine evaluates conditions against the service's decoded data on every
cycle (a PLC frame for tsend2mqtt, a poll cycle for iolinkmaster2mqtt) and
fires MQTT actions and/or incidents when conditions are met. Rules are
loaded once at startup and are designed for zero-allocation operation on the hot
path.
Each service references its own rules file from its config.yaml:
rules_file: /path/to/rules.xml
#XML schema
<rules>
<rule name="..." cooldown="..." edge="...">
<!-- exactly one top-level condition: <cond>, <and>, or <or> -->
<actions>
<publish topic="..." payload='...'/>
</actions>
<incident source="..." severity="..." summary="..."/>
</rule>
</rules>
#<rule> attributes
| Attribute | Required | Description |
|---|---|---|
name |
yes | Unique rule identifier. Used in the incident dedup_key and in log messages. |
cooldown |
no | Minimum time between firings. Go duration string (e.g. 30s, 1m, 500ms). Default 0 (no cooldown). Applies to actions and incident triggers, but not to incident resolves. |
edge |
no | rising or none (default). See Edge detection. |
A rule must have <actions>, <incident>, or both.
#Conditions
Each rule has exactly one top-level condition element: <cond>, <and>, or
<or>. Conditions can be nested up to 4 levels deep, with a maximum of 16
children per <and>/<or>.
#<cond> — leaf condition
Compares a single decoded field against a value.
<cond device="vibration1" tag="temperature" op="gt" value="50.0"/>
| Attribute | Required | Description |
|---|---|---|
device |
IO-Link only | The IO-Link device name from config.yaml ports. Used by iolinkmaster2mqtt, which hosts multiple devices. tsend2mqtt has a single implicit PLC source and omits device. |
tag |
yes | Field name from the service's decoded output (see Fields below). |
op |
yes | Comparison operator (see table below). |
value |
depends | Comparison value. Required for all operators except changed. |
On tsend2mqtt (a single PLC source), omit device — a condition is just
tag, op, and value:
<cond tag="Bool137" op="eq" value="true"/>
#Operators
| Operator | Aliases | Types | Description |
|---|---|---|---|
eq |
=, == |
boolean, number, integer, string | Equal |
neq |
!= |
boolean, number, integer, string | Not equal |
lt |
< |
number, integer | Less than |
leq |
<= |
number, integer | Less than or equal |
gt |
> |
number, integer | Greater than |
geq |
>= |
number, integer | Greater than or equal |
changed |
— | any | Field value changed since the last cycle. No value attribute needed. |
XML escaping: When using
<or<=in an XML attribute value, use the word forms (lt,leq) or XML-escape them (<,<=).
Boolean and string fields support only eq and neq.
#<and> / <or> — logic groups
Combine multiple conditions. Short-circuit evaluation (AND stops on the first false, OR stops on the first true).
<and>
<cond device="vibration1" tag="alert_temp" op="eq" value="true"/>
<cond device="vibration1" tag="temperature" op="geq" value="50.0"/>
</and>
Groups can be nested:
<or>
<and>
<cond device="vibration1" tag="alert_vrms_max" op="eq" value="true"/>
<cond device="vibration1" tag="temperature" op="gt" value="40.0"/>
</and>
<cond device="vibration1" tag="alert_acc_peak" op="eq" value="true"/>
</or>
#Fields
The comparison value is parsed to match the field's type. Which fields and
types are available depends on the service.
#iolinkmaster2mqtt (IO-Link)
Field types come from the driver's decoded output (JSON Schema types):
| Schema type | Example value |
Notes |
|---|---|---|
boolean |
true, false |
Alert bits |
number |
5.0, 36.5 |
Float32 sensor values (temperature, RMS, …) |
integer |
0, 224 |
Raw alert word, counters |
string |
hello |
String fields (eq/neq only) |
Example — SICK MPB10 vibration sensor:
| Tag | Type | Description |
|---|---|---|
alert_temp |
boolean | Temperature alert bit |
alert_acc_peak |
boolean | Acceleration peak alert bit |
alert_vrms_prewarn |
boolean | V-RMS prewarn alert bit |
alert_vrms_max |
boolean | V-RMS max alert bit |
alerts_raw |
integer | Raw 16-bit alert word |
vrms_x, vrms_y, vrms_z |
number | Velocity RMS (default v-rms profile) |
temperature |
number | Sensor temperature in °C |
Slot fields depend on the active profile; the default v-rms profile provides
vrms_x, vrms_y, vrms_z, and temperature.
#tsend2mqtt (PLC)
Tags are the field names from the .udt definition (exact match). The value
is parsed based on the tag's S7 type:
| S7 type | Parsed as | Example |
|---|---|---|
| Bool | boolean | true, false, 1, 0 |
| Int, DInt, SInt, USInt, UInt, UDInt, Byte, Word, DWord | integer | 42, -1, 0 |
| Real, LReal | number | 85.0, -1.5, 0 |
| String, WString | string | hello |
#Actions
Actions are ordered <publish> elements inside <actions>. They fire
sequentially (fire-and-forget, QoS 0, non-retained). If payload is omitted,
{} is used.
<actions>
<publish topic="sensingcam/sick1/trigger" payload='{"event_name":"alarm"}'/>
<publish topic="iolink/vibration1/param/reset_alerts" payload='{}'/>
</actions>
Actions publish to absolute topics (no prefix prepended). This lets a rule
trigger external systems (cameras, other services) or send commands back to a
device — for IO-Link, ISDU commands via the iolink/<name>/param/<command>
topic.
#Edge detection
The edge attribute controls when actions fire:
| Edge | Behavior |
|---|---|
none (default) |
Fire every cycle where the condition is true (subject to cooldown). |
rising |
Fire only on the false→true transition. Does not re-fire while the condition stays true. |
For most alerting use cases, rising is recommended to avoid repeated firings.
Incidents always use rising-edge semantics regardless of this setting (see
below).
#Cooldown
The cooldown attribute sets a minimum interval between firings, using Go
duration syntax:
30s → 30 seconds
1m → 1 minute
1m30s → 1 minute 30 seconds
500ms → 500 milliseconds
Cooldown applies to action firing (both none and rising modes) and
incident triggers. Cooldown does not apply to incident resolves — resolves
are never suppressed.
#Incidents
The optional <incident> element enables the trigger/resolve lifecycle for
human-facing alerting. Incidents are published to the common incidents/ topic
and surfaced in the Edge Hub web interface; see the
Incident Protocol for the full contract.
<incident source="vibration1" severity="warning"
summary="Vibration1 alarm triggered"/>
| Attribute | Required | Description |
|---|---|---|
source |
yes | The data source (must match a configured device/port). Used to build the source ID: {topic_prefix}_{source}. |
severity |
yes | critical, error, warning, or info. |
summary |
yes | Human-readable description (max 120 characters). |
#Lifecycle
Incidents always use rising-edge semantics, regardless of the rule's edge
setting:
- Trigger (condition becomes true): publishes a trigger message to
incidents/with QoS 1. - Resolve (condition becomes false): publishes a resolve message to
incidents/. Resolves are never suppressed by cooldown.
If cooldown suppresses a trigger, no active flag is set, so no orphan resolve is sent when the condition later clears.
#Message format
Trigger:
{
"action": "trigger",
"dedup_key": "iolink_vibration1-vibration1-alarm",
"source": "iolink_vibration1",
"severity": "warning",
"summary": "Vibration1 alarm triggered",
"time": "2026-07-11T12:34:56.789Z",
"data": {"rule": "vibration1-alarm"}
}
Resolve:
{
"action": "resolve",
"dedup_key": "iolink_vibration1-vibration1-alarm",
"source": "iolink_vibration1",
"time": "2026-07-11T12:35:26.789Z",
"data": {"rule": "vibration1-alarm"}
}
The dedup_key format is {topic_prefix}_{source}-{rule_name}, matching the
discovery ID scheme.
#Cross-device rules
A condition may reference more than one source:
<rule name="both-overtemp" cooldown="60s" edge="rising">
<and>
<cond device="vibration1" tag="temperature" op="gt" value="50.0"/>
<cond device="vibration2" tag="temperature" op="gt" value="50.0"/>
</and>
<actions>
<publish topic="alerts/critical" payload='{"alert":"both_overtemp"}'/>
</actions>
</rule>
#Limits
| Limit | Value |
|---|---|
| Maximum rules | 1000 |
| Maximum nesting depth | 4 |
Maximum children per <and>/<or> |
16 |
| Maximum incident summary length | 120 characters |
#Examples
#IO-Link (iolinkmaster2mqtt)
<rules>
<!-- Action + incident: vibration max alarm while running warm. -->
<rule name="vibration1-alarm" cooldown="45s" edge="rising">
<or>
<and>
<cond device="vibration1" tag="alert_vrms_max" op="eq" value="true"/>
<cond device="vibration1" tag="temperature" op="gt" value="40.0"/>
</and>
<cond device="vibration1" tag="alert_acc_peak" op="eq" value="true"/>
</or>
<actions>
<publish topic="sensingcam/sick1/trigger" payload='{"event_name":"alarm"}'/>
<publish topic="iolink/vibration1/param/reset_alerts" payload='{}'/>
</actions>
<incident source="vibration1" severity="warning"
summary="Vibration1 alarm triggered"/>
</rule>
</rules>
#PLC (tsend2mqtt)
The same schema, with PLC tags from the .udt and no device attribute (a
single PLC source):
<rules>
<!-- Action + incident: alarm bit trips a camera AND raises an incident. -->
<rule name="alarm-camera" cooldown="45s" edge="rising">
<cond tag="Bool137" op="eq" value="true"/>
<actions>
<publish topic="camera/record" payload='{"duration":40}'/>
</actions>
<incident source="plc1" severity="critical"
summary="PLC alarm Bool137 active"/>
</rule>
<!-- Compound AND, incident only: alarm active while temperature high. -->
<rule name="alarm-hot" cooldown="30s" edge="rising">
<and>
<cond tag="Bool137" op="eq" value="true"/>
<cond tag="Real70" op="geq" value="85.0"/>
</and>
<incident source="plc1" severity="critical"
summary="Alarm active while temperature high"/>
</rule>
<!-- Action only: fire when the alarm code changes to a non-zero value. -->
<rule name="new-alarm-code" cooldown="5s" edge="rising">
<and>
<cond tag="Int93" op="changed"/>
<cond tag="Int93" op="neq" value="0"/>
</and>
<actions>
<publish topic="alerts/alarm_code" payload='{"event":"new_code"}'/>
</actions>
</rule>
</rules>
The <incident source="…"> still names the PLC source (used to build the
dedup_key); only the per-condition device attribute is IO-Link-specific.