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

# Wallboard alarm rules

> Define the thresholds that fire supervisor alarms on queue metrics — waiting callers, wait times, service level, abandon rate, staffing — and understand how they evaluate, fire, and clear.

A wallboard alarm rule tells the supervisor wallboard **when to escalate a queue metric from a stat into an alarm** — for example, "the longest wait on Support exceeds 120 seconds", or "tenant-wide service level drops below 80%". Rules are evaluated every five seconds, fire while a breach persists, and clear automatically when the metric recovers. Wire the rules you want supervised, tune them over time, and delete the noise.

**Base path:** `/api/v1/voice/wallboard/alarm-rules`

**Scope:** `voice` read to list; `voice` write to create, update, and delete.

***

## The shape of a rule

Every rule has a **metric**, a **comparator**, and a **threshold**, plus a few optional fields:

* **`queue_id`** — the queue this rule watches. Omit it (or send `null`) for a **tenant-wide rule** that applies to every queue. Tenant-wide rules are handy for org-level cutoffs like "service level below 80% anywhere".
* **`name`** — a label you recognise on the wallboard (1–120 characters).
* **`metric`** — one of:
  * `waiting` — callers currently waiting in the queue
  * `longest_wait` — current oldest caller's wait, in seconds
  * `avg_wait` — average wait across waiting callers, in seconds
  * `predicted_wait` — the dispatch engine's predicted wait for a caller entering now (longest current wait plus the typical handle time), in seconds
  * `agents_available` — agents who can take the next call right now
  * `agents_total` — agents on the roster for the queue
  * `service_level` — SL achievement percentage, computed over a rolling 30-minute window
  * `abandoned_rate` — abandoned-call percentage, computed over a rolling 30-minute window
* **`comparator`** — one of `gt`, `gte`, `lt`, `lte`, `eq`.
* **`threshold`** — the numeric boundary the comparator evaluates against. For a percentage metric (`service_level`, `abandoned_rate`) send a fraction from `0` to `1` (send `0.8` for 80%); for the wait-time metrics it is seconds.
* **`duration_seconds`** — how long the breach must persist before the alarm fires. `0` (the default) fires on the first breaching tick; anything up to 86,400 (24h) is accepted.
* **`channel`** — how the alarm surfaces: `sse` (default, live wallboard events), `sse+notification` (also a browser notification), `webhook`, or `email`.
* **`enabled`** — set it `false` to mute a rule without deleting it (defaults to `true`).

<Note>
  Both an alarm and its recovery state are evaluated against **live queue stats**; `service_level` and `abandoned_rate` are the only metrics backed by a rolling window (30 minutes). A rule on either of them is skipped on any tick where the window carried no traffic, rather than firing on a meaningless 0%.
</Note>

## Create a rule

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/voice/wallboard/alarm-rules" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "queue_id": null,
    "name": "Longest wait > 120s",
    "metric": "longest_wait",
    "comparator": "gt",
    "threshold": 120,
    "duration_seconds": 30,
    "channel": "sse+notification"
  }'
```

`201` returns the created rule row. When a rule with the same `queue_id`, `metric`, `comparator`, and `threshold` already exists, the conflict resolves to the existing row with `200` — a double-click does not create a duplicate.

## List and filter rules

```bash cURL theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/voice/wallboard/alarm-rules?queue_id=queue_supp" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

`200` returns every rule for the tenant. Pass `queue_id` to narrow to the rules **actually evaluated for that queue** — its queue-scoped rules plus the tenant-wide rules, which is exactly the set the evaluator applies.

Each rule in the response carries its `id`, the resolved `queue_id` / `queue_name` (`null` for tenant-wide), `name`, `metric`, `comparator`, `threshold`, `duration_seconds`, `channel`, `enabled`, the creator's `created_by_user_id`, and `created_at` / `updated_at` timestamps.

## Update a rule

Patch the fields you want to change; at least one field is required. Send `queue_id: null` to move a queue-scoped rule to tenant-wide.

```bash cURL theme={null}
curl -X PATCH "https://api.orbit.devotel.io/api/v1/voice/wallboard/alarm-rules/rule_01H..." \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "threshold": 90, "enabled": false }'
```

`200` returns the updated rule row; `404` when no rule with that id exists; `409` when the edited shape collides with another rule.

## Delete a rule

Hard-remove a rule; the wallboard stops raising it at the next evaluation.

```bash cURL theme={null}
curl -X DELETE "https://api.orbit.devotel.io/api/v1/voice/wallboard/alarm-rules/rule_01H..." \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

`204` on success, `404` when the id is unknown. Deletion is permanent — if you want to silence a rule temporarily, set `enabled: false` instead.

***

## Evaluation lifecycle

A background evaluator ticks **every five seconds** — the same cadence the live wallboard refreshes on — and evaluates every rule for every tenant on every tick.

Per tick, per queue, the applicable rule set is the **queue-scoped rules plus every tenant-wide rule** (exactly what a `queue_id`-filtered list call returns). A rule change you push with PATCH takes effect within five seconds of the next tick.

**What `duration_seconds` means precisely:** with `0`, the alarm fires on the first tick where the condition holds. With any value above `0`, the condition must hold **continuously** for that many seconds — the breach timer starts on the first breaching tick, and a single non-breaching tick resets it. Only once the timer elapses does the alarm fire. This is what filters out momentary spikes: a rule that would otherwise flap on a single tick instead demands a sustained breach.

## Alarm lifecycle

1. **Armed** — the rule is being evaluated on every tick.
2. **Breaching** — the condition holds but `duration_seconds` has not yet elapsed; nothing is shown to supervisors.
3. **Fired** — the duration gate has elapsed and the alarm surfaces. A breach that persists re-fires **at most once every 30 seconds** per (queue, rule), so a stuck queue produces a fresh event rather than a flood.
4. **Recovered** — the moment the metric returns to the comparator-safe side, the alarm stops re-firing, the breach timer is reset, and a future breach starts the duration clock fresh.

An alarm **clears automatically on recovery** — there is no explicit acknowledge/clear action, and no adjacency counted for supervisors to chase. The wallboard's live feed receives each fired event; `sse+notification` additionally raises a browser notification so a backgrounded tab still pages the supervisor. Recovery means silence, not a persistent badge.

## Choosing thresholds

Two worked examples — a tenant-wide staffing floor and a queue-level wait guard:

**Tenant-wide service-level floor.** Watch the org-wide SL and page when it drops:

```json theme={null}
{
  "name": "Service level below 80%",
  "queue_id": null,
  "metric": "service_level",
  "comparator": "lt",
  "threshold": 0.8,
  "duration_seconds": 600
}
```

Because `service_level` is a rolling 30-minute figure, a long `duration_seconds` (10 minutes here) keeps the alarm from firing on a thin early-morning window.

**Queue-level wait guard.** Watch a single queue for callers waiting too long:

```json theme={null}
{
  "name": "Support longest wait > 120s",
  "queue_id": "queue_support",
  "metric": "longest_wait",
  "comparator": "gt",
  "threshold": 120,
  "duration_seconds": 30
}
```

<Tip>
  Avoid a bare `longest_wait` threshold on queues that absorb overflow calls by design — those callers arrive already holding accumulated wait, so every overflow arrival trips the alarm. Use `avg_wait` or a higher `longest_wait` threshold on overflow-absorbing queues.
</Tip>

## Choosing a channel

All four channel values still emit the same live `wallboard.alarm_triggered` event to the in-app wallboard feed; the `channel` field controls **where the alarm additionally lands when the supervisor is off the wallboard tab**:

* **`sse`** — live feed only. Best for rules a supervisor watches on a mounted wallboard.
* **`sse+notification`** — live feed plus a browser notification (if the supervisor has granted permission). Pick this when the wallboard might be backgrounded.
* **`webhook`** — additionally POSTs the alarm to every webhook subscription registered for the `wallboard.alarm_triggered` event. Use this to drive PagerDuty/Opsgenie-style paging or a chat-ops script. The delivered body is a standard signed envelope wrapping the alarm payload:

```json Webhook body theme={null}
{
  "id": "event_01H...",
  "type": "wallboard.alarm_triggered",
  "created_at": "2026-08-25T10:15:31Z",
  "data": {
    "rule_id": "wbalarm_01H...",
    "rule_name": "Support longest wait > 120s",
    "queue_id": "queue_support",
    "queue_name": "Support",
    "metric": "longest_wait",
    "comparator": "gt",
    "threshold": 120,
    "value": 142.5,
    "channel": "webhook",
    "duration_seconds": 30
  }
}
```

* **`email`** — additionally sends an immediate email to the organization's admin users, subject "Queue alarm: \<queue name> — \<rule name>", body describing the metric, comparator, threshold, and current value (percentages are rendered as whole numbers, so `abandoned_rate` at threshold `0.05` currently at `0.07` reads "abandoned\_rate ≥ 5% breached — current value 7%"). Use this for breach classes an on-duty ops-bridge list should page on.

## Troubleshooting

**The alarm never fires.** Check in order:

1. The rule's `enabled` flag — a muted rule is loaded by list calls but skipped by the evaluator.
2. The scope — a queue-scoped rule cannot fire on queue `B` when its `queue_id` points at queue `A`; tenant-wide rules have `queue_id: null`. List with `?queue_id=` for the queue you expect to fire on and confirm the rule appears.
3. `duration_seconds` — a long window means the breach must hold continuously before anything fires; a short-lived spike never clears the gate.
4. Comparator/threshold orientation — `lt` on `service_level` with `threshold: 80` never fires because `service_level` thresholds are fractions in `0..1` (correct: `threshold: 0.8`). Verify the threshold you stored with a GET list call.
5. Metric resolvability — rules on `service_level` / `abandoned_rate` skip ticks where the 30-minute window carried no traffic (that's by design, to avoid firing on a fabricated 0%).

**The alarm fires constantly.** The breach is re-firing at most once every 30 seconds while it persists — if that's too noisy, raise `duration_seconds` so only sustained breaches fire, loosen the threshold, or mute the rule with `enabled: false` while you retune. If the underlying metrics themselves look wrong (impossible SL percentages, waits that don't match the floor), check the [voice call quality troubleshooting](/troubleshooting/voice-call-quality) guide before retuning thresholds against bad data.

## Governance

Creating, updating, and deleting rules requires the `voice` **write** scope; listing requires `voice` read. Rule changes take effect within one evaluation tick.

Prefer **soft-delete** (`enabled: false`) when you want to silence a rule you may need again — a seasonal staffing alarm, a queue you're about to restructure. Use DELETE for rules created in error or permanently obsolete; deletion is hard, and the audit value lies in keeping the rule's shape retrievable from list history rather than recreating it.

For percentage metrics, always confirm the stored `threshold` is the `0..1` fraction form before attributing an alarm gap to rule logic — a stored `80` reads to evaluators as "breach only if service level falls below 8000%," which is unreachable.
