Skip to main content
How-to

Set Up Report Alarms

Overview

An alarm is a condition attached to a report that decides whether the run is worth telling anyone about. Ten reports on a weekly schedule are ten things nobody reads by week four. With alarms, the reports keep running and you hear about them only when a number crosses a line you drew.

Alarms evaluate as part of the ordinary uc-bq run pipeline, so there is nothing extra to schedule or host. They use the same Slack channels and email recipients the report already has, but produce a visually distinct notification so recipients do not go report-blind.

Add your first alarm

Attach an alarm to a report that already exists:

uc-bq config add-alarm revenue-by-payment \
--name "Revenue Drop" \
--type pct_change \
--metric total_revenue \
--aggregate sum \
--operator "<" \
--value -20 \
--severity high \
--cooldown 24h

Then run the report as usual:

uc-bq run revenue-by-payment --deliver

If total revenue fell by more than 20 percent against the previous run, an alarm notification goes to the report's delivery channels. Otherwise nothing extra happens.

Check what you configured with uc-bq config show-alarms revenue-by-payment. Every alarm flag is listed in the CLI reference.

Test before you trust it

Evaluate the alarms against the data already on disk, with no query and no notifications:

uc-bq alarm test revenue-by-payment

This is the fastest way to confirm that a metric name matches a column in data.json and that your operator points the way you meant. Review what has fired historically with uc-bq alarm history revenue-by-payment.

Choose an alarm type

TypeFires whenNeeds
thresholdAn aggregated metric crosses a fixed valuemetric, aggregate, operator, value
pct_changeAn aggregated metric moves by more than a percentage against the previous runmetric, aggregate, operator, value
missing_dataThe query returns zero rowsNothing beyond a name

Threshold

Aggregates one column across every row of data.json and compares the result against a fixed number:

alarms:
- name: "Low Revenue"
type: threshold
metric: "total_revenue"
aggregate: "sum"
operator: "<"
value: 10000
severity: critical
cooldown: "24h"

The aggregate is one of sum, avg, min, max, first, or last, and the operator is one of <, >, <=, >=, ==, or !=. Choosing min turns a whole-report check into a per-row one, which is how you catch a single bad line item:

# Any product selling below cost
- name: "Negative Margin"
type: threshold
metric: "margin"
aggregate: "min"
operator: "<"
value: 0
severity: critical
cooldown: "24h"

# Unusually large orders, often a fraud signal
- name: "High AOV"
type: threshold
metric: "avg_order_value"
aggregate: "avg"
operator: ">"
value: 500
severity: high
cooldown: "24h"

Percent change

Compares this run's aggregate against the most recent value recorded in alarm_state.json:

alarms:
- name: "Revenue Drop"
type: pct_change
metric: "total_revenue"
aggregate: "sum"
operator: "<"
value: -20
compare_to: "previous_run"
severity: high
cooldown: "24h"

A value of -20 with operator < reads as "alert if the metric declined by more than 20 percent". Point the operator the other way to catch spikes:

- name: "Ad Spend Spike"
type: pct_change
metric: "total_spend"
aggregate: "sum"
operator: ">"
value: 50
compare_to: "previous_run"
severity: high
cooldown: "7d"

Two behaviours are worth knowing before you rely on this type. On the very first run there is nothing to compare against, so the alarm is skipped and the metric is recorded for next time. If the previous value was zero, any non-zero value counts as an infinite change and fires; zero to zero does not.

Missing data

Fires when the query comes back with no rows at all, which usually means a broken pipeline or a query that silently stopped matching anything:

alarms:
- name: "No Orders"
type: missing_data
severity: critical
cooldown: "24h"

No metric, operator, or value is needed. For an active merchant, zero orders is never a legitimate result, which makes this the cheapest early warning you can add.

Pick a severity

Severity decides how loudly the notification arrives, so match it to how quickly somebody has to act.

  • low rides along with the normal report delivery and gets no special formatting. Use it for things worth noting, such as ad spend running 10 percent over budget.
  • high produces a separate notification: a coloured Slack attachment and an email subject prefixed with [ALARM]. Use it for anything that needs attention the same day.
  • critical does everything high does and adds the Slack mention configured by mention_on_alarm. Reserve it for act-now conditions such as zero orders in 24 hours.

Set the mention once per report:

uc-bq config set-mention-on-alarm revenue-by-payment "@channel"

Stop repeat notifications

A condition that persists would otherwise fire on every run. Cooldown suppresses the repeats:

cooldown: "24h" # the default
cooldown: "7d" # weekly summary alarms
cooldown: "30m" # high-frequency monitoring
cooldown: "0" # no suppression, fire every time

Units are m for minutes, h for hours, and d for days. When an alarm fires, the time is recorded in alarm_state.json; later runs inside the window still evaluate as triggered but stay quiet. When the metric recovers, the suppression is cleared, so the next genuine trigger fires immediately rather than waiting out the old window.

Deliver only when an alarm fires

Set the report's delivery mode to alarm_only to turn a scheduled report into a silent one:

uc-bq config set-delivery-mode revenue-by-payment alarm_only

The report still runs, the data still refreshes, and the chart is still rendered. Nothing is sent unless an alarm triggers, at which point both the alarm notification and the report itself arrive. See Report delivery.

Decide what to do with alarm state

Alarm definitions live in report.yaml and belong in Git. Alarm state lives beside it in alarm_state.json and holds the metric history used for percent-change comparisons, capped at 30 entries, plus the current suppression records.

Keeping the two files separate means state changes do not clutter the diff on your report definition. How you handle the state file depends on where reports run:

  • Working locally, add reports/**/alarm_state.json to .gitignore. The history stays on your machine, which is all it needs to do.
  • Running in GitHub Actions, commit the file back after each run, or cache it as a workflow artifact. A fresh checkout has no history, so every percent-change alarm would be treated as a first run and skipped.

Roll alarms up across a deck

When a deck runs several reports, their alarms are collected into one notification rather than arriving separately:

Deck: Weekly Executive Briefing
[1/3] Revenue by Payment Method ... OK
ALARM [HIGH] Revenue Drop: total_revenue changed -23.4% (47230 to 36189)
[2/3] Customer LTV ................. OK
[3/3] Product Performance .......... OK
ALARM [CRITICAL] No Orders: Query returned zero rows

The combined message names which report raised which alarm, then the deck PDF is delivered as normal. A deck can be set to alarm_only too:

uc-bq config set-deck-delivery-mode weekly-executive alarm_only
Was this page helpful?