Skip to content

Anomaly Detection

View .md

Anomaly detection changes when a subscription notifies. Instead of its notification trigger, Beacon compares each run’s row count with the row counts of the subscription’s earlier runs and sends the notification only when the run is unusual.

It is statistical (standard deviation, interquartile range or percentage change), part of Beacon.Core, and needs no LLM.

Anomaly detection is set when you create the subscription; a subscription cannot be edited afterwards.

  1. In Subscriptions (/beacon/subscriptions), click New subscription.
  2. On the Anomaly step, tick Enable anomaly detection.
  3. Set the fields below and finish the subscription.
FieldDefaultDialog acceptsWhat it does
Detection methodStandard deviationStandard deviation, IQR, Percentage changeHow a run is compared with the baseline. See Methods.
SensitivityMediumLow, Medium, HighThe threshold of the method. High alerts more often. IQR ignores it.
Lookback (days)307–365Age of the baseline runs: only runs from the last N days count.
Minimum data points73–100Baseline runs needed before any run can be an anomaly.
Alert on an unusual increaseonAlert when the row count is unusually high.
Alert on an unusual decreaseonAlert when the row count is unusually low. The dialog requires at least one of the two.

The ranges and the either/or rule are the dialog’s. Through the API, the same settings go in the anomalyConfig object of POST /beacon/api/subscriptions (detectionMethod, sensitivity, lookbackDays, minimumDataPoints, alertOnIncrease, alertOnDecrease), and the API stores the values it is given.

For a subscription with anomaly detection on, each run that executes its query goes through these steps:

  1. Evaluate. Beacon loads the stored baseline values of the subscription from the last Lookback days. With fewer than Minimum data points values, the run is not an anomaly (“Not enough historical data … Building baseline…”). Otherwise the configured method decides.
  2. Decide the notification. The usual checks come first: a run that timed out is Timeout, a run with 0 rows is NoResults, and a run below the subscription’s minimum row count is BelowThreshold. After those, an anomaly sends the notification (NotificationSent) and any other run is silenced (NotificationSilenced). The subscription’s notification trigger is not used while anomaly detection is on. A run whose query returns no rows ends as NoResults; anomaly detection compares runs that return rows. The Control Tower shows NoResults runs.
  3. Store the baseline. The run’s row count is stored as a new baseline value, with the time of the run. A run is not stored when all of its recipients wait for approval of their destination, so nobody could be told about it.
  4. Record the event. For an anomaly, Beacon records an anomaly event (current value, baseline mean, standard deviation, z-score, severity, explanation), linked to the first notification that was delivered, if any. Recipients receive the evaluation with the notification; the webhook payload carries it as Anomaly.

Baselines are stored only while anomaly detection is on for the subscription. Runs that did not execute the query (skipped because the owner may no longer run it, or because the query’s change is not approved) store nothing.

Tasks are independent of anomaly detection: they follow the Create a task option of the subscription and open on any run with rows.

All three methods use the baseline values of the lookback window, oldest to newest, without the current run.

Beacon computes the mean (μ) and the population standard deviation (σ) of the baseline, then the z-score of the run: z = (x − μ) / σ. The run is an anomaly when |z| is above the sensitivity’s threshold and its direction (above or below the mean) has alerting turned on.

SensitivityThreshold
High1.5σ
Medium2σ
Low3σ

Severity by |z|: Critical from 4, High from 3, Medium from 2, otherwise Low. When every baseline value is the same (σ = 0), the z-score is 0 and no run is an anomaly.

Baseline: [10, 12, 11, 13, 10, 12, 11] mean 11.29, σ 1.03
Run: 18 rows z = (18 − 11.29) / 1.03 = 6.5
Medium: 6.5 > 2 → anomaly, severity Critical

Suits steady data.

Beacon sorts the baseline and takes Q1 and Q3 (the values at 25% and 75% of the sorted list), then IQR = Q3 − Q1. The run is an anomaly when it is below Q1 − 1.5 × IQR (with decrease alerts on) or above Q3 + 1.5 × IQR (with increase alerts on). The multiplier is fixed at 1.5; Sensitivity does not change it.

Severity by how many IQRs the run lies beyond the bound: Critical from 3, High from 2, Medium from 1, otherwise Low.

Baseline (sorted): [8, 10, 11, 12, 13, 14, 16, 20] Q1 11, Q3 16, IQR 5
Bounds: 11 − 7.5 = 3.5 to 16 + 7.5 = 23.5
Run: 35 rows → above 23.5 → anomaly

Suits history with occasional spikes.

Beacon compares the run with the mean of the baseline (not with the previous run): change = |x − mean| / mean. The run is an anomaly when the change is above the sensitivity’s threshold and its direction has alerting turned on.

SensitivityThreshold
High15%
Medium25%
Low40%

Severity by change: Critical from 100%, High from 50%, Medium from 25%, otherwise Low. When the baseline mean is 0, any run with rows is an anomaly (severity High).

Baseline mean: 100
Run: 140 rows → change 40%
Medium: 40% > 25% → anomaly, severity Medium

Suits business volumes where a relative change is what matters.

DataMethod
Steady counts with little variationStandard deviation
Counts with occasional extreme valuesIQR
Volumes where a relative change matters (orders, sign-ups)Percentage change
ProblemWhat to change
Alerts on normal variationLower the sensitivity (Low), try IQR, or lengthen the lookback.
Real changes go unnoticedRaise the sensitivity (High), or use Percentage change for sudden jumps.
Detection never startsThe subscription has fewer baseline runs in the lookback window than Minimum data points. Wait for more runs, or create the subscription again with a lower minimum or a longer lookback.
Only increases (or only decreases) matterTurn off the other direction.

The lookback window counts days, not runs. A subscription that runs hourly with a 7-day lookback has up to 168 baseline values; one that runs daily has 7. Set Minimum data points to what the schedule can deliver within the lookback.

  • Subscription page, Anomaly detection tab (shown while detection is on). The method, sensitivity, lookback, minimum data points and alert directions; a chart of the last 30 days of row counts with the baseline and the lower and upper thresholds; and the recent detection events.
  • Control Tower. An Anomaly badge on subscriptions with detection on, a per-day sparkline of anomaly events and the anomaly count, and the recent anomalies in the row detail. See Control Tower.

The chart route is GET /beacon/api/subscriptions/{id}/anomaly-chart?days=30. It needs read access to the subscription, and its run points cover only runs the caller may read.

Three entities in the beacon schema hold anomaly data (PostgreSQL uses snake_case names):

EntityHolds
AnomalyConfigOne per subscription: Enabled, DetectionMethod, Sensitivity, LookbackDays, AlertOnIncrease, AlertOnDecrease, MinimumDataPoints.
AnomalyBaselineOne per stored run: SubscriptionId, ExecutionTime, MetricValue (the row count).
AnomalyEventOne per detected anomaly: SubscriptionId, NotificationId, DetectedTime, CurrentValue, BaselineMean, BaselineStdDev, ZScore, Severity, Explanation, Acknowledged, AcknowledgedBy.

IAnomalyDetectionService (in Beacon.Core.Services) is what the subscription run uses. A host can call it in-process:

MethodWhat it does
EvaluateAnomalyAsync(subscriptionId, rowCount)Evaluates a row count against the stored baseline.
StoreBaselineAsync(subscriptionId, metricValue, executionTime)Stores a baseline value.
GetHistoricalMetricsAsync(subscriptionId, lookbackDays)Baseline values of the window, oldest first.
RecordAnomalyEventAsync(subscriptionId, evaluation, notificationId)Records an anomaly event.
GetAnomalyConfigAsync(subscriptionId)The configuration as AnomalyConfigData, or null.
SaveAnomalyConfigAsync(AnomalyConfigData)Creates or updates the configuration of a subscription.
DeleteAnomalyConfigAsync(subscriptionId)Removes the configuration.
GetAnomalyChartDataAsync(subscriptionId, lookbackDays, runScope)Chart data, limited to the runs runScope may read.

AnomalyEvaluationResult has IsAnomaly, CurrentValue, BaselineMean, BaselineStdDev, ZScore, Explanation, Severity and HistoricalDataPoints. BaselineStdDev and ZScore are set by the standard deviation method only.

await anomalyDetectionService.SaveAnomalyConfigAsync(new AnomalyConfigData
{
SubscriptionId = subscriptionId,
Enabled = true,
DetectionMethod = AnomalyDetectionMethod.PercentageChange,
Sensitivity = AnomalySensitivity.Medium,
LookbackDays = 30,
MinimumDataPoints = 14,
AlertOnIncrease = true,
AlertOnDecrease = true
}, cancellationToken);

SaveAnomalyConfigAsync does not check who calls it; a host that exposes it decides who may change a subscription’s configuration.