Event Processing & Alarms
DeviceChain turns raw device telemetry into meaningful signals. A dedicated event-processing service watches events as they flow through the pipeline: a DETECT stage evaluates streaming rules in real time, and a REACT stage runs the automated responses each firing declares — raising an alarm (a stateful condition with a lifecycle, a severity, and a path to notify a human) or issuing a command back to the device.
The service is built for replay-correctness: it evaluates on event time and persists its state, so a restart re-derives identical firings — none missed, none duplicated.
Available today: the DETECT + REACT pipeline is the platform's live detection engine. Detection covers threshold, held-for-duration, repeating-occurrence, rate-of-change, silence/absence, connectivity, windowed-aggregate, and area/group correlation conditions (with static or attribute-driven thresholds); REACT actions are raise alarm, send command, call a webhook, and publish to a connector, with per-action guards. Rules are authored three ways over one schema — a typed form builder, a visual automation canvas, and, with the AI service enabled, a natural-language "Describe" door — all validated by the same compiler before publish, and the canvas can preview a draft against replayed history before it goes live. The four-state alarm lifecycle, severity tiers with in-place escalation, live alarm and detection subscriptions, and email/webhook notification with escalation are all in place.
Where rules live
Detection rules are defined on a device profile — the versioned capability contract shared by one or more device types. Because the profile is versioned (draft → publish → rollback), a change to a fleet's detection logic is authored as a draft, published atomically, and rolled back if needed, exactly like the profile's metric and command definitions. Every device that resolves to the profile picks up its rules automatically.
A rule states a condition over the profile's telemetry, declares its severity, and lists the actions to run when it fires.
Scoping a rule to a group
By default a rule applies to every device that resolves to its profile. A rule can also be scoped to a dynamic group so it fires only for the devices that are currently members — for example, run a stricter heat rule only on devices in arid areas. Scoping is optional and set per rule.
Group membership is recorded on each event as it is resolved, so the engine sees exactly which rules applied at that moment — including when it replays history to preview or re-derive firings. When a device joins or leaves the group, it is enrolled or dropped on its next event, with no rule edit and no rescan.
Condition types
| Condition | Fires when | Parameters |
|---|---|---|
| Threshold | a reading crosses a comparison (e.g. temperature > 80) | the comparison + a threshold value |
| Duration | the condition holds continuously for at least a set time (e.g. pressure low for 5 minutes) | a hold time |
| Repeating | the condition occurs a number of times within a window (e.g. 3 faults in 10 minutes) | an occurrence count + a window |
| Rate of change | a metric changes too fast between consecutive readings (e.g. temperature rising > 5°/s) | the comparison + an optional flag to normalise the change to a per-second rate |
| Absence / silence | a device goes quiet — no qualifying event within a window (a dead-man check) | a silence window |
| Connectivity | a device reports an authoritative disconnect (raise) and reconnects (resolve) — for presence-asserting transports like Sparkplug-B and LwM2M. Defined through the API today — neither console authoring surface offers it yet, and one of them will silently rewrite it (see below). | none — the presence edge is the whole signal |
| Windowed aggregate | an aggregate over a window crosses a comparison (e.g. average > 50 over 10 minutes) | the function (count/sum/avg/min/max), a window (tumbling, sliding, or session), the comparison + value |
| Area correlation | enough distinct devices in an area meet the condition together (e.g. ≥ 3 devices in a zone report a fault within 5 minutes) | the area/anchor type, a distinct-device count + window |
Each condition's comparison can be a structured metric · operator · value leaf or an advanced CEL expression over the event; both are statically type-checked and cost-limited when the profile is published, so a malformed or runaway rule is rejected before it can run.
Every time span a rule declares — a window, a hold time, a silence timeout, a session gap — is capped at 24 hours by default. A rule asking for longer is refused when the profile is published, naming the field and the limit.
The cap exists because a windowed rule keeps one record per reading for the whole window, per device, in an engine shared by every tenant. A multi-day window over a fleet reporting every few seconds is a large amount of memory held indefinitely, and the cost is paid by everyone on the instance rather than by the tenant that authored it.
Silence timeouts and session gaps are capped too, even though they hold no readings: those rules re-arm a timer every time a device reports, and the superseded timers are not released until their deadline passes — so a long timeout under frequent reporting accumulates in the same way.
If you need a longer span, an operator can raise the limit for the instance
(maxRuleDurationSeconds) after sizing the
memory. Before asking for one, consider whether the question is really about retention rather
than detection — a "compare against last month" question is usually better answered by querying
stored history than by holding a month of readings in memory.
Because the form builder does not model the Connectivity type, opening an existing Connectivity rule in it silently reads the rule back as a Threshold rule, and saving from that drawer replaces the original definition. Nothing warns you: the form's "could not be read" notice fires only when the stored definition is not valid JSON, which a working Connectivity rule of course is.
The automation canvas is safe — it refuses the rule outright and tells you the type is unsupported. Until the form builder learns the type, edit a Connectivity rule only through the API.
Static and dynamic thresholds
A threshold can be a fixed value on the rule, or dynamic — the name of a device attribute the rule reads at evaluation time. A dynamic threshold lets one rule adapt per device: the profile defines the rule once, and each device carries its own limit as a SERVER- or SHARED-scoped attribute (server-set values take precedence). Change the attribute and the effective threshold changes with no rule edit.
Automated actions
When a rule fires, its REACT actions run. The built-in actions are:
- Raise alarm — open (or escalate) a stateful alarm for the device, described below. This is the default and needs no target beyond a severity.
- Send command — enqueue a command back to the device through the persistent command pipeline (dispatch is idempotent, so a replay or retry never double-sends).
- Call a webhook (
httpCall) — POST a CEL-shaped payload to an external HTTP endpoint, with hardened delivery (redirects refused, reserved headers stripped) and optional secret-store auth. - Publish to a connector (
publish) — hand a CEL-shaped payload to an outbound connector that fans it out to a message broker or cloud queue (MQTT, Kafka, AWS SNS/SQS).
The last two — the outbound actions — are described in Outbound Connectors; they are delivered by a separate service so a slow external system never slows detection.
A rule can carry several actions (up to a small fixed limit), and each action can be guarded by a condition on the firing — so, for example, one rule can raise an alarm on every firing but send a command only when the reading is in a particular band. Because a firing is edge-triggered (a rising edge when the condition starts holding, a falling edge when it stops), an alarm raised on the rising edge is cleared automatically on the falling edge — you author the raise, and the clear is implied.
Authoring & previewing rules
Rules are authored in the console three ways, all over the same schema and all validated by the same server-side compiler before publish:
- A form builder — a typed form per condition type, the quickest path for a single rule. As you edit, it shows the compiler's type and cost feedback inline, before you publish.
- A visual automation canvas — a node graph (source → condition → optional branches → actions) for richer flows. The canvas compiles to the same rule a form would produce; it is an authoring surface, not a second engine. It adds branch nodes (route a firing to different actions by a guard) and compute nodes (name a reusable derived value and reference it in a condition or guard).
- A natural-language "Describe" door — where the AI service is enabled, describe the rule in words and have a candidate drafted for you to review and publish. It is offered when you create a new rule, and it produces a rule in the same schema the other two doors produce. See AI-Assisted Authoring.
The canvas's standout is preview against history: run a draft rule over the profile's replayed event history and see the raise/resolve edges it would have produced over a chosen window — without publishing anything. Selecting a firing overlays a per-node trace onto the canvas, showing the path that event took (which condition matched, which branch it took, which action fired). You can edit and re-preview until the rule does what you expect, then publish.
The alarm lifecycle
A raised alarm is a stateful object, not a one-off message. Its state is a combination of two axes — a four-state model:
- State —
ACTIVEwhile the condition holds,CLEAREDonce it resolves. - Acknowledged — whether an operator has taken ownership of the alarm (with a record of who and when).
So an alarm moves through ACTIVE/unacknowledged → ACTIVE/acknowledged → CLEARED, and a flapping condition re-activates the same alarm rather than spawning duplicates.
An alarm names the device that raised it, and alarms are queried tenant-wide with filters (state, severity, acknowledgement, originating device) rather than read off a parent entity.
Severity and escalation
Each alarm carries a severity — CRITICAL, MAJOR, MINOR, WARNING, or INDETERMINATE. A single condition can declare rules at several severity tiers (for example temp > 80 → MAJOR, temp > 100 → CRITICAL); the engine escalates a single active alarm in place to the highest tier currently met, and de-escalates as conditions ease — rather than opening a separate alarm per tier.
Reaching a human
A raised alarm can notify people through the notification system. A per-tenant policy routes alarms to email (SMTP) and webhook channels with per-severity routing — each of a policy's rules maps one severity (or any) to a channel and a recipient list. Escalation is per policy, not per severity: the policy sets a single interval and cap, and an alarm that stays neither acknowledged nor cleared is re-notified through the same channels on that schedule until the cap. An alarm carries one escalation clock and tier however many policies match it, so the shortest interval among them paces all of them — a severity cannot be given a cadence of its own. Policies also support throttling, a minimum gap between notifications for the same alarm so a repeatedly-signalling alarm does not flood a channel. Where two policies route to the same channel and an identical recipient list, the duplicate delivery is collapsed and the notification is sent once — the same recipients in a different order, or in different letter case, are treated as distinct and both are sent. Channel credentials (the SMTP password, a webhook bearer token) are held in the platform's encrypted secret store — sealed at rest with envelope encryption, write-only over the API, and never returned as cleartext. This machine-to-human path is kept distinct from the machine-to-machine outbound connectors that fan events out to other systems.
Seeing alarms & rule health
Alarms surface live in two places without any extra wiring:
- The console Alarms view — a live, tenant-wide list, filterable and acknowledgeable in place.
- Dashboard widgets — a live alarm table and an alarm count widget (see Dashboards), including acknowledge/clear actions that the server authorizes against the operator's own rights.
Both are fed by live subscriptions, so state changes appear as they happen.
A profile's own editor also shows rule health — per-rule status, last-fired time, and fire count — alongside a live feed of detections as they occur, so you can confirm a newly published rule is behaving before it ever raises an alarm.
Running it
The service that evaluates rules holds live state in memory and runs as a single instance, which gives it a few operational properties worth knowing before you depend on it in production — what a restart costs, how quickly a silence rule can fire, why an alarm might not clear, and how to find a rule that is failing to evaluate. Those are covered in Running the Detection Engine.