/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 sendnull) 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 queuelongest_wait— current oldest caller’s wait, in secondsavg_wait— average wait across waiting callers, in secondspredicted_wait— the dispatch engine’s predicted wait for a caller entering now (longest current wait plus the typical handle time), in secondsagents_available— agents who can take the next call right nowagents_total— agents on the roster for the queueservice_level— SL achievement percentage, computed over a rolling 30-minute windowabandoned_rate— abandoned-call percentage, computed over a rolling 30-minute window
comparator— one ofgt,gte,lt,lte,eq.threshold— the numeric boundary the comparator evaluates against. For a percentage metric (service_level,abandoned_rate) send a fraction from0to1(send0.8for 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, oremail.enabled— set itfalseto mute a rule without deleting it (defaults totrue).
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%.Create a rule
cURL
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
cURL
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. Sendqueue_id: null to move a queue-scoped rule to tenant-wide.
cURL
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.cURL
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 aqueue_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
- Armed — the rule is being evaluated on every tick.
- Breaching — the condition holds but
duration_secondshas not yet elapsed; nothing is shown to supervisors. - 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.
- 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.
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: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:
Choosing a channel
All four channel values still emit the same livewallboard.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 thewallboard.alarm_triggeredevent. 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:
Webhook body
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, soabandoned_rateat threshold0.05currently at0.07reads “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:- The rule’s
enabledflag — a muted rule is loaded by list calls but skipped by the evaluator. - The scope — a queue-scoped rule cannot fire on queue
Bwhen itsqueue_idpoints at queueA; tenant-wide rules havequeue_id: null. List with?queue_id=for the queue you expect to fire on and confirm the rule appears. duration_seconds— a long window means the breach must hold continuously before anything fires; a short-lived spike never clears the gate.- Comparator/threshold orientation —
ltonservice_levelwiththreshold: 80never fires becauseservice_levelthresholds are fractions in0..1(correct:threshold: 0.8). Verify the threshold you stored with a GET list call. - Metric resolvability — rules on
service_level/abandoned_rateskip ticks where the 30-minute window carried no traffic (that’s by design, to avoid firing on a fabricated 0%).
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 guide before retuning thresholds against bad data.
Governance
Creating, updating, and deleting rules requires thevoice 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.