> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ilyama.golain.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Alarm kinds and conditions

> Reference for alarm definition logic — the three kinds, SQL queries and their limits, CEL expressions, timing, no-data handling, labels, and asset scope.

An alarm definition says **what to watch** (its `kind` and query), **when it fires** (`condition_cel`), **when it resolves**, and **how long each must hold**. This page is the reference for those fields. For the concepts behind them, see [Alarms overview](/alarms/overview). For a guided first rule, see the [first alarm walkthrough](/alarms/first-alarm).

## Kinds

Set `kind` on the definition. An empty `kind` is treated as `event`.

| `kind` | Watches | Required fields | How the condition is decided | One instance per |
| - | - | - | - | - |
| `threshold` | Rows returned by a SQL query over a dataspace, on a schedule | `source_dataspace_id`, `query_spec.sql`, `condition_cel` | `condition_cel` runs against **each result row** | Distinct `labels_by` values (or one per definition when `labels_by` is empty) |
| `event` with a query | Whether a SQL query returns anything, on a schedule | `query_spec.sql`, `condition_cel` | **One or more rows** = condition met. **Zero rows** = not met. No CEL runs. | Definition's `asset_id`, or one for the whole project |
| `event` without a query | Each change to a row in a `state` dataspace | `source_dataspace_id`, `condition_cel` | `condition_cel` runs against the changed row | Dataspace row |
| `staleness` | MQTT devices disconnected from the broker | `staleness_window_seconds` > 0, `condition_cel` | Device disconnected for longer than the window | Device |

> [!NOTE]
>
> `condition_cel` is required by the API for every kind. For an `event` rule with a query and for a `staleness` rule, the evaluator never runs it. Send `"true"` — the Console does the same.

### Threshold

A threshold rule runs its SQL every evaluation interval against the dataspace's read view. Each row the query returns is evaluated on its own:

1. The row's columns are exposed to CEL as `event.<column>`.
2. `condition_cel` decides whether that row is breaching.
3. The values of the `labels_by` columns identify which alarm instance the row belongs to.

Shape the query so it returns **one row per thing you want to alarm on** — for example one row per device with `GROUP BY device_id`, and `labels_by: ["device_id"]`.

A column named `value` is special: when present and numeric, it is recorded as the instance's trigger value and is what the deadband compares against.

### Event

An event rule works in one of two ways, depending on whether it has a query.

**With a query**

The rule runs `query_spec.sql` every evaluation interval. If the query returns at least one row, the condition is met. If it returns no rows, the condition is not met. CEL is not evaluated on the result.

There is one instance for the definition's `asset_id`, or a single project-wide instance when `asset_id` is empty. Use this when "any row matches" is the alarm — for example any error-level log line in the last five minutes.

**Without a query**

The rule watches the dataspace named in `source_dataspace_id` and is evaluated each time a row in a `state` dataspace changes. `condition_cel` runs against the changed row. Each row gets its own instance.

Deleting a row counts as **no data** for that row's instance — see [No data handling](#no-data-handling).

An event rule with neither a query nor a source dataspace never evaluates.

### Staleness (device offline)

A staleness rule fires for each device that has been disconnected longer than `staleness_window_seconds`.

| Behavior | Detail |
| - | - |
| **Scope** | Every enabled MQTT device in the project. One instance per device. |
| **Offline means** | The broker recorded a disconnect (clean, error, or keepalive expiry). It is not based on the device's last-seen time. |
| **Never connected** | A device that has never connected counts as offline since it was created. |
| **Check frequency** | About once a minute, regardless of `eval_interval_seconds`. |
| **Reconnect** | Resolves the device's instance immediately, without waiting for the next check. |

> [!TIP]
>
> LoRaWAN, HTTP, and OPC-UA devices hold no broker connection, so they are never "offline" in this sense. To catch a device that stops sending data, use a `threshold` rule over `max(event_ts)`. The Console's **Telemetry stops arriving** template builds that query for you — see [Telemetry stops arriving](#telemetry-stops-arriving) below.

## Query spec

`query_spec` carries the native SQL a `threshold` rule (or an `event` rule with a query) runs:

```json theme={null}
{
  "kind": "sql",
  "sql": "SELECT device_id, avg(temperature) AS value FROM ds_read_3f9c2a71 WHERE event_ts >= now() - INTERVAL 5 MINUTE GROUP BY device_id"
}
```

| Key | Value |
| - | - |
| `kind` | `sql` |
| `sql` | A read-only `SELECT` against a dataspace **read view** |

Name the read view in `FROM`, not the dataspace name. Copy it from the dataspace catalog's `read_view` field (it looks like `ds_read_<hex>`). The Console shows it above the SQL editor. See [Dataspaces overview](/dataspaces/overview).

### Query limits

Every scheduled query must stay small, because rules share one evaluator. When you create or update a definition, the SQL is checked and rejected if it breaks any of these rules:

| Rule | Detail |
| - | - |
| **Bound `event_ts` with a relative window** | The query must filter `event_ts` against `now()`. |
| **Window of 24 hours or less** | A larger window is rejected. `MONTH` and `YEAR` units are always over the limit. |
| **No absolute time** | Dates in quotes (for example `'2026-01-01'`) and `toDateTime…`, `parseDateTime…`, or `fromUnixTimestamp…` calls are rejected. |
| **Must plan** | The query is dry-run against the read views. Unknown columns or functions, or a dataspace the rule's author cannot read, are rejected with the reason. If the check itself cannot run, the definition is not saved — retry. |
| **Finish in time** | Each evaluation must finish within its evaluation interval, clamped to between 5 and 60 seconds. A slower query fails that evaluation. |

Accepted window forms:

```sql theme={null}
event_ts >= now() - INTERVAL 15 MINUTE
event_ts >= now() - toIntervalHour(4)
event_ts >= subtractMinutes(now(), 30)
event_ts >= now() - 900               -- seconds
now() - INTERVAL 15 MINUTE <= event_ts
```

## CEL expressions

`condition_cel`, `resolve_cel`, and `clear_cel` are [CEL](https://cel.dev) expressions. Each must evaluate to a boolean, or the definition fails to evaluate.

| Variable | Type | Contents |
| - | - | - |
| `event` | map | The row being evaluated. Read columns as `event.<column>`. |
| `asset` | map | `asset.id` — the asset the instance belongs to. |
| `alarm` | map | The open instance, if any: `alarm.condition_state` (`pending`, `firing`, `resolved`, `latched`) and `alarm.trigger_value`. Empty when no instance is open. |
| `now` | timestamp | Evaluation time. |

The CEL math and strings extensions are available.

> [!WARNING]
>
> A bare column such as `event.alarm_active` is not known to be a boolean and is rejected. Compare it explicitly: `event.alarm_active == true`.

Examples:

| Expression | Fires when |
| - | - |
| `event.value > 80.0` | The row's `value` is above 80 |
| `event.temperature > 75.0 && event.humidity > 60.0` | Both readings are high |
| `math.abs(event.pressure - 101.3) > 5.0` | Pressure is more than 5 away from nominal |
| `event.status == 'fault'` | A status column reads `fault` |
| `event.fw_version.startsWith('1.')` | A device is still on a 1.x firmware |
| `has(event.error_code) && event.error_code != 0` | The row carries a non-zero error code |

### Deciding when an alarm resolves

The resolve candidate is chosen in this order:

1. **`resolve_cel`** — if set, the alarm starts resolving when this is true. Use it for an asymmetric band: fire at `event.value > 80.0`, resolve at `event.value < 70.0`.
2. **`clear_cel`** — if set (and `resolve_cel` is not), the alarm starts resolving when this is true.
3. **`deadband`** — if neither expression is set, and both `deadband` and a `setpoint` in `metadata` are greater than zero, the alarm starts resolving when the row's `value` is at or below `setpoint - deadband`.
4. **Otherwise** — the alarm starts resolving when `condition_cel` is false.

`resolve_cel`, `clear_cel`, and `deadband` apply to `threshold` rules and to `event` rules without a query. Scheduled `event` rules resolve when the query returns no rows. `staleness` rules resolve when the device reconnects.

## Timing and hysteresis

| Field | Default | What it does |
| - | - | - |
| `eval_interval_seconds` | `60` | How often a `threshold` or scheduled `event` rule runs. Runs in whole minutes: anything under 60 runs every minute, and other values round down to whole minutes. Ignored by `staleness` rules and by `event` rules without a query. |
| `for_seconds` | `0` | How long the condition must hold before the instance moves from `pending` to `firing`. `0` fires on the first breach. |
| `keep_firing_for_seconds` | `0` | How long the resolve candidate must hold before a firing instance resolves. |
| `on_delay_seconds` | `0` | Used in place of `for_seconds` when `for_seconds` is `0`. |
| `off_delay_seconds` | `0` | Used in place of `keep_firing_for_seconds` when that is `0`. |
| `re_alarm_delay_seconds` | `300` | After an instance fires and closes, a new instance for the same labelset is not opened within this many seconds. Stops flapping. |
| `eval_delay_seconds` | `0` | For `event` rules without a query: extra wait for late data before a `for` or `keep_firing_for` hold is checked. |
| `latching` | `false` | When `true`, a firing instance moves to `latched` instead of `resolved` once it clears, and stays open until an operator releases it. |

Hold times are measured from the instance's own timestamps, so they survive restarts. On scheduled rules a hold can only mature on an evaluation, so the effective delay rounds up to the next evaluation.

> [!NOTE]
>
> If the condition goes false while an instance is still `pending`, the instance closes without ever firing. A `resolved` instance that breaches again fires again immediately — `for_seconds` gates only fresh activations.

## No data handling

`on_no_data` decides what happens to an **open** instance when its labelset produces no row on a `threshold` evaluation, or when its row is deleted on an `event` rule without a query.

| `on_no_data` | Meaning |
| - | - |
| `keep_firing` (default) | Keep the current state. A firing alarm keeps firing; a disappeared series does not resolve itself. |
| `resolve` | Treat no data as the condition clearing. |
| `alert` | Treat no data as the condition being met. |

No data only affects labelsets that already have an open instance. A labelset that has never produced a row cannot raise an alarm through `alert`. Scheduled `event` rules and `staleness` rules always have data and ignore this field.

## Labels

`labels_by` lists the result columns whose values identify an instance on a `threshold` rule. Rows with the same values for those columns update the same instance; a new combination opens a new one. The values are stored on the instance as its labels and shown on the board.

| `labels_by` | Result |
| - | - |
| `[]` (empty) | One instance for the whole rule (per `asset_id`, if set) |
| `["device_id"]` | One instance per device |
| `["device_id", "channel"]` | One instance per device and channel |

Every column in `labels_by` should appear in the query's `SELECT`.

## Scoping to assets

| Field | Effect |
| - | - |
| `asset_id` | `event` without a query: only that asset's rows are evaluated. `event` with a query and `threshold`: the instance is attached to that asset, and with empty `labels_by` there is one instance for it. |
| `asset_type_id` | `event` without a query: only rows whose asset is of that type are evaluated. |
| neither | Project-wide. |

`staleness` rules always cover every enabled MQTT device in the project.

## Examples

```json Threshold theme={null}
{
  "key": "boiler-overtemp",
  "name": "Boiler over temperature",
  "kind": "threshold",
  "source_dataspace_id": "8b0f6d0e-2a43-4c55-9b1f-6a2d1e7c9a10",
  "query_spec": {
    "kind": "sql",
    "sql": "SELECT device_id, avg(temperature) AS value FROM ds_read_3f9c2a71 WHERE event_ts >= now() - INTERVAL 5 MINUTE GROUP BY device_id"
  },
  "condition_cel": "event.value > 80.0",
  "resolve_cel": "event.value < 70.0",
  "labels_by": ["device_id"],
  "eval_interval_seconds": 60,
  "for_seconds": 120,
  "keep_firing_for_seconds": 120,
  "re_alarm_delay_seconds": 300,
  "on_no_data": "keep_firing",
  "priority": 2
}
```

```json Event theme={null}
{
  "key": "controller-errors",
  "name": "Controller reported an error",
  "kind": "event",
  "asset_id": "c41e7b52-9f0a-4d1e-8a77-3b5f2e6d0c18",
  "query_spec": {
    "kind": "sql",
    "sql": "SELECT event_ts, message FROM ds_read_7a1d04be WHERE event_ts >= now() - INTERVAL 5 MINUTE AND level = 'error' LIMIT 1"
  },
  "condition_cel": "true",
  "eval_interval_seconds": 60,
  "re_alarm_delay_seconds": 300
}
```

```json Staleness theme={null}
{
  "key": "device-offline",
  "name": "Device offline",
  "kind": "staleness",
  "staleness_window_seconds": 1200,
  "condition_cel": "true",
  "response_procedure": "Check the device's power and its gateway link."
}
```

Send any of these to `POST /projects/{project_id}/alarm-definitions` — see [API reference](/alarms/api-reference).

### Telemetry stops arriving

For devices that have no broker connection, alarm on silence with a `threshold` rule. This is the query the Console's **Telemetry stops arriving** template generates for a 20-minute window:

```sql theme={null}
SELECT device_id, max(event_ts) AS last_ts
FROM ds_read_3f9c2a71
WHERE event_ts >= now() - INTERVAL 24 HOUR
GROUP BY device_id
HAVING max(event_ts) < now() - INTERVAL 20 MINUTE
```

Pair it with `condition_cel: "true"`, `labels_by: ["device_id"]`, and `on_no_data: "resolve"`. A device appears in the result only while it is quiet, so it resolves as soon as rows resume.

> [!WARNING]
>
> Because the window is capped at 24 hours, a device silent for longer than a day drops out of the result and its alarm resolves. Use a `staleness` rule for MQTT devices where that matters.

## Next steps

> **[Notifications](/alarms/notifications)**
>
> Get paged when an instance fires, with re-notify and escalation.

> **[Silences and shelving](/alarms/silences-and-shelving)**
>
> Suppress alarms during maintenance or known faults.

> **[Console guide](/alarms/console-guide)**
>
> Author definitions in **Alarms** → **Definitions**.

> **[Troubleshooting](/alarms/troubleshooting)**
>
> Rules that never fire, rejected queries, and flapping alarms.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.