BMS alarm triage agent

548 alarms in. 18 things to look at.

A building automation system can raise hundreds of alarms a day, and most of them are noise. This agent collapses the repeats, ranks what is left, checks every condition against the trend data, and hands an engineer a short list with a reason and a next diagnostic step for each. It recommends only. It has no write path to any building system.

548alarm events
32distinct conditions
18items to review
data
holdout split, 3 windows, synthetic
explanations
Claude Haiku, 11 of 15; template, 4
built
2026-10-20 17:14

About the written explanations. This copy was written by Claude Haiku (claude-haiku-5-5) at build time: one request per card, all sent as one batch, each given the same structured evidence the local model is given. Every reply was then checked by the same guardrails the local model's text must pass: allowed verbs only, the point itself as the target, no control actions. 11 of 15 replies passed and are used here. The other 4 failed a check or got no reply, so those cards were written by the fixed template from the condition's own measurements instead, and each card says which wrote it. The batch ran 2026-10-16 14:05 UTC; this copy replays its saved replies, checked again when the page was built. Every number, rank, chart and rule is the same whichever writer is used.

The same alarms, before and after

From a wall of alarms to a reading list

Left is every alarm event the agent was handed, one row per point, the way a queue presents them. Right is what it hands back.

Before548 alarm events on 32 pointsWhat the queue shows. Every tick is one alarm, return, or repeat. Nothing says which ones matter.
00:0004:0008:0012:0016:0020:00
After18 items, in reading orderRanked escalations, plus the ones it could not judge (?). 14 set aside as nuisance, each with the rule that decided it.
1AHU-21/HTG_DUCT_TEMP2AHU-21-B/CLG_DUCT_TEMP3AHU-21/CLG_DUCT_TEMP4AHU-21/MIXED_AIR_TEMP5AHU-21/CLG_SP?AHU-21-B/HTG_DMPR_POS1BLR-71/STACK_TEMP2BLR-71-B/HW_SUP_TEMP3BLR-71/HW_SUP_TEMP4BLR-71/HW_RET_TEMP5BLR-71/FIRING_RATE?BLR-71-B/COMB_AIR_DP1CH-61-B/CHW_SUP_TEMP2CH-61/CHW_RET_TEMP3CH-61/CHW_SUP_TEMP4CH-61/COND_APPROACH5CH-61/EVAP_PRESS?CH-61-B/OIL_PRESS00:0004:0008:0012:0016:0020:00

Nine steps, fixed order

How a window gets worked

Almost all of it is ordinary deterministic code, so the ranking does not change run to run. The model only writes the two sentences on each card.

  1. N1IngestReads the alarm export and the trend export off disk.
  2. N2ValidateChecks both cover the same window and the same points.
  3. N3NormalizeTurns both into one internal schema.
  4. N4CollapseGroups repeat alarms into distinct conditions and tags ISA-18.2 nuisance patterns.
  5. N5RankScores each condition on alarm-log evidence alone.
  6. N6Fetch evidencePulls the matching trend series for every condition.
  7. N7Reassessthe agent stepSix fixed rules read the trend and promote or demote. The first to fire sets the band.
  8. N8Explainonly model callThe one model call. Writes why it matters and what to check next.
  9. N9ReportWrites this page, the markdown report, and the run log.

Scored against labeled faults, holdout split

The scorecard

Every fault in these windows was labeled when the data was generated. The agent never sees those labels. Scoring happens outside it, with the same code the benchmark uses.

PASS15 of 15Real faults in each window's top 5at least 12 required
PASS30.4xFewer items to review548 alarm events down to 18 items
PASS0.0%False escalations0 of 15 escalations wrong, ceiling 20%
ApproachFaults in top 5Items to reviewVolume cutFalse escalations
Raw alarm queueevery event, newest first155481.0x62.2%
Deduplication scriptcollapses repeats, cannot decline92918.9x48.3%
This agentcollapses, ranks, checks the trend, declines151830.4x0.0%

Grading what the writers say

Evals

The ranking above is checked against labeled faults. This checks the words on each card. Each case is a frozen triage result with its right answer: the root cause, what is a consequence, what should escalate, what should stay unresolved, and the key evidence. Each writer is given the same prompts, its replies are accepted the way the agent accepts them, and six fixed graders score the result. No grader is a model.

GraderTemplateClaude Haiku, own wordsClaude Haiku, as served
Grounding100.0%40 of 40100.0%16 of 16100.0%16 of 16
Root cause95.0%38 of 40100.0%16 of 16100.0%16 of 16
Escalation94.8%55 of 58100.0%34 of 34100.0%34 of 34
Uncertainty100.0%45 of 4576.2%16 of 21100.0%21 of 21
Next step100.0%40 of 4081.2%13 of 16100.0%16 of 16
Format100.0%40 of 40100.0%16 of 16100.0%16 of 16

The template was graded on all 58 cases on 2026-10-20; 55 passed every grader. Every failure left is the pipeline's, in 3 hard cases: the words are right, and the set of cards is not. Claude Haiku was graded on the 34 cases its saved replies cover; the other 24 need an API key and have not been run. As served means through the service's guardrails, which held back 5 of its replies and had the template write those cards.

What each grader checks

Grounding
Every point name, number and time on a card has to be in the evidence the writer was given. Anything invented, converted or carried in from elsewhere fails.
Root cause
The write-up names the right cause, says what is wrong with it from the evidence, and never turns a consequence into the cause. With no cause in the evidence, it names none.
Escalation
What should escalate gets a card and nothing else does, and no card talks its own escalation down as nuisance or nothing to act on.
Uncertainty
Where evidence is missing or thin the write-up says so: unsupported items stay unresolved, thin cards say what they rest on, and no card guesses a diagnosis, claims certainty, or rules out what it could not check.
Next step
Every card has a next step, it is about that equipment and what the point measures, and it is read only: it observes, and never tells anyone to change, reset, acknowledge or operate anything.
Format
Each reply passes the agent's Pydantic schema on the first try, and the reason and the next step are one sentence each, as the prompt asks.

Two failures the eval found, and the fixes

Ruling out a cause it could not check

Template, Uncertainty U5. Case power-outage-utility-trend-missing
Template wroteUPS on battery at UPS-A is confirmed in the trend from 10:08:00 UTC to 10:08:14 UTC (about 14 seconds), and no utility failure, sag, or overload in this export explains it, so it needs its own look.
After the fixUPS on battery at UPS-A is confirmed in the trend from 10:08:00 UTC to 10:08:14 UTC (about 14 seconds), and no confirmed utility failure, sag, or overload in this export explains it, so it needs its own look.

The export held a utility failure alarm whose trend was missing. The evidence said no confirmed cause explains the battery alarm; the card said there was no utility failure at all, which points an operator away from the likeliest cause. One word, taken from the evidence, fixes it.

An alarm-log ranking that read as trend-confirmed

Claude Haiku, Uncertainty U2. Case hvac-chiller-ch-61-evap-press
Claude Haiku wroteThe evidence-side shows the evaporator pressure point of CH-61 was below its 58.0 psi LOW limit for 50.0% of the window (about 11.5 hours) and was still past the limit at window end, with a peak deviation of 2.40 deadbands, so the condition persisted rather than clearing.
Served insteadNo trend rule moved this one, so it ranks on the alarm log alone: CH-61's evaporator pressure raised 4 alarms at priority 3 over 11.5 hours in alarm, and it was still past the limit when the export ended.

No trend rule confirmed this condition, and Claude Haiku's reply never said so. A new guardrail now holds back any reply that drops that limit, and the template, which states it, writes the card instead. Haiku's own words still fail this check; only a new prompt can change those.

The cases are synthetic. They come from the generated HVAC holdout and the hand-sequenced electrical journals, plus composites built from the same plant model, and the right answers were written for them. A high score shows the writers handle these shapes. It is not evidence about a real building's alarms. Clarity to an operator is the one optional check a model grades, with a written rubric, and it is kept out of these numbers.

The output

Each window, ranked

Pick a window. Each card shows the trend the decision rests on, where the alarm log alone ranked it, where it landed after the trend check, and the rule that moved it. Open How it got here for the full score and rule trace.

Alarm window

AHU, dual duct

holdout_ahu_dual_duct, about 22 hours from 28 Jul 2026

142alarm events
10distinct conditions
6items to review
5 to look at4 set aside as nuisance1 it could not judge

Look at these first

1
AHU-21 · default alarm class · priority 2 · low alarm

SYN1/AHU-21/HTG_DUCT_TEMP

▲ Promoted by trendR-P3 Excursion magnitude
alarm log alone#1 of 10
held
after trend check#1
80.090.000:0004:0008:0012:0016:0020:00limit 88.0 degF
trendalarm limitdeadband 1.50 degFin alarmalarm / return

Why it matters

The heating duct temperature stayed below its 88.0 degF low limit for about 53% of the window and was still below it at the end, with a peak deviation of 9.20 deadbands, indicating a sustained low-temperature condition on AHU-21 that the evidence shows as persistent rather than transient.

Check next

Verify SYN1/AHU-21/HTG_DUCT_TEMP: Look for whether the duct temperature trend remains below the 88.0 degF limit through the end of the window, and confirm the period of deviation and the peak excursion of about 9.20 deadbands against the logged samples.

events collapsed
14
alarms / repeats / returns
5 / 5 / 4
time in alarm
12.3 h
alarm-side score
0.787
How it got here

Alarm-side score, 0.787

Weighted from the alarm log alone, before any trend data is read. The trend check never changes this number.

priority0.75+0.263
active duration1.00+0.250
repeat count1.00+0.150
alarm count0.50+0.125
nuisance category0.00-0.000

Trend rules, first to fire sets the band

  1. R-D1Trend within deadband demotedid not firepeak overshoot 9.20 deadbands exceeds the 1.00 allowed
  2. R-D2Excursion below minimum duration demotedid not firetotal 46080 s beyond the limit meets the 600 s minimum
  3. R-D3Self recovered and stable demotedid not firestill beyond the limit at the end of the segment
  4. R-P1Sustained excursion promotedid not firebeyond the limit for 53.3 percent of the segment, under the 60.0 percent required
  5. R-P2Monotonic drift promotedid not firereverses by 14.06 deadbands, more than the 1.00 allowed for a monotonic drift
  6. R-P3Excursion magnitude promoteset the bandpeak deviation 9.20 deadbands past the limit reaches the 4.00 required
Your call
2
AHU-21-B · critical alarm class · priority 2 · high alarm

SYN1/AHU-21-B/CLG_DUCT_TEMP

▲ Promoted by trendR-P3 Excursion magnitude
alarm log alone#2 of 10
held
after trend check#2
60.070.000:0004:0008:0012:0016:0020:00limit 60.0 degF
trendalarm limitdeadband 1.50 degFin alarmalarm / return

Why it matters

The cooling discharge duct temperature alarm stayed above its 60.0 degF high limit for about 53% of the window, peaked about 9.17 deadbands past the limit, and was still past the limit at the end of the window, so the cooling condition is sustained rather than brief.

Check next

Verify SYN1/AHU-21-B/CLG_DUCT_TEMP: Check whether the trend shows the duct temperature staying above 60.0 degF through the late part of the window, and whether the 11 repeats and 4 returns to normal line up with the sustained high readings.

events collapsed
20
alarms / repeats / returns
5 / 11 / 4
time in alarm
12.3 h
alarm-side score
0.787
How it got here

Alarm-side score, 0.787

Weighted from the alarm log alone, before any trend data is read. The trend check never changes this number.

priority0.75+0.263
active duration1.00+0.250
repeat count1.00+0.150
alarm count0.50+0.125
nuisance category0.00-0.000

Trend rules, first to fire sets the band

  1. R-D1Trend within deadband demotedid not firepeak overshoot 9.17 deadbands exceeds the 1.00 allowed
  2. R-D2Excursion below minimum duration demotedid not firetotal 46080 s beyond the limit meets the 600 s minimum
  3. R-D3Self recovered and stable demotedid not firestill beyond the limit at the end of the segment
  4. R-P1Sustained excursion promotedid not firebeyond the limit for 53.3 percent of the segment, under the 60.0 percent required
  5. R-P2Monotonic drift promotedid not firereverses by 12.66 deadbands, more than the 1.00 allowed for a monotonic drift
  6. R-P3Excursion magnitude promoteset the bandpeak deviation 9.17 deadbands past the limit reaches the 4.00 required
Your call
3
AHU-21 · critical alarm class · priority 2 · high alarm

SYN1/AHU-21/CLG_DUCT_TEMP

▲ Promoted by trendR-P1 Sustained excursionalarm-side: stale
alarm log alone#4 of 10
moved up 1
after trend check#3
55.060.065.000:0004:0008:0012:0016:0020:00limit 60.0 degF
trendalarm limitdeadband 1.50 degFin alarmalarm / return

Why it matters

The cooling duct temperature stayed above its 60.0 degF high limit for 85.4 percent of the window and was still above it at the end, so the condition is persistent rather than a brief excursion, and the alarm has never returned to normal.

Check next

Verify SYN1/AHU-21/CLG_DUCT_TEMP: Confirm the trend shows the duct temperature held above 60.0 degF across most of the 20-hour window, with a peak about 5.17 deadbands past the limit and no return below the 1.5 degF deadband.

events collapsed
21
alarms / repeats / returns
1 / 20 / 0
time in alarm
20.0 h
alarm-side score
0.487
How it got here

Alarm-side score, 0.487

Weighted from the alarm log alone, before any trend data is read. The trend check never changes this number.

priority0.75+0.263
active duration1.00+0.250
repeat count1.00+0.150
alarm count0.10+0.025
nuisance category1.00-0.200

Trend rules, first to fire sets the band

  1. R-D1Trend within deadband demotedid not firepeak overshoot 5.17 deadbands exceeds the 1.00 allowed
  2. R-D2Excursion below minimum duration demotedid not firetotal 73800 s beyond the limit meets the 600 s minimum
  3. R-D3Self recovered and stable demotedid not firestill beyond the limit at the end of the segment
  4. R-P1Sustained excursion promoteset the bandbeyond the limit for 85.4 percent of the segment and still beyond it at the end, a standing condition
  5. R-P2Monotonic drift promotefiredmoves away from the limit at 0.353 deadbands per hour and never reverses by more than 0.33 deadbands, so it is degrading rather than holding a fixed offset
  6. R-P3Excursion magnitude promotefiredpeak deviation 5.17 deadbands past the limit reaches the 4.00 required
Your call
4
AHU-21 · default alarm class · priority 3 · high alarm

SYN1/AHU-21/MIXED_AIR_TEMP

▲ Promoted by trendR-P2 Monotonic drift
alarm log alone#6 of 10
moved up 2
after trend check#4
70.075.000:0004:0008:0012:0016:0020:00limit 72.0 degF
trendalarm limitdeadband 1.00 degFin alarmalarm / return

Why it matters

AHU-21's mixed air temperature moved steadily toward and past its 72 degF limit without reversing, peaking 3.2 deadbands out, and the single alarm it raised understates a condition that is getting worse.

Check next

Review SYN1/AHU-21/MIXED_AIR_TEMP: trace the trend back to where the drift began, then compare the value with a handheld thermometer at the sensor to separate a drifting sensor from a drifting process.

events collapsed
1
alarms / repeats / returns
1 / 0 / 0
time in alarm
7.9 h
alarm-side score
0.365
How it got here

Alarm-side score, 0.365

Weighted from the alarm log alone, before any trend data is read. The trend check never changes this number.

priority0.50+0.175
active duration0.66+0.165
alarm count0.10+0.025
repeat count0.00+0.000
nuisance category0.00-0.000

Trend rules, first to fire sets the band

  1. R-D1Trend within deadband demotedid not firepeak overshoot 3.24 deadbands exceeds the 1.00 allowed
  2. R-D2Excursion below minimum duration demotedid not firetotal 30060 s beyond the limit meets the 600 s minimum
  3. R-D3Self recovered and stable demotedid not firestill beyond the limit at the end of the segment
  4. R-P1Sustained excursion promotedid not firebeyond the limit for 34.8 percent of the segment, under the 60.0 percent required
  5. R-P2Monotonic drift promoteset the bandmoves away from the limit at 0.381 deadbands per hour and never reverses by more than 0.09 deadbands, so it is degrading rather than holding a fixed offset
  6. R-P3Excursion magnitude promotedid not firepeak deviation 3.24 deadbands is under the 4.00 required
Your call
5
AHU-21 · default alarm class · priority 3 · high alarm

SYN1/AHU-21/CLG_SP

● Trend left it unchanged
alarm log alone#3 of 10
moved down 2
after trend check#5
1.301.401.501.6000:0004:0008:0012:0016:0020:00limit 1.50 inH2O
trendalarm limitdeadband 0.04 inH2Oin alarmalarm / return

Why it matters

No trend rule moved this one, so it ranks on the alarm log alone: AHU-21's cooling static pressure raised 4 alarms at priority 3 over 11.5 hours in alarm, and it was still past the limit when the export ended.

Check next

Check SYN1/AHU-21/CLG_SP: compare the reading with a manometer at the sensor tap, and look at why it alarmed as often as it did.

events collapsed
7
alarms / repeats / returns
4 / 0 / 3
time in alarm
11.5 h
alarm-side score
0.515
How it got here

Alarm-side score, 0.515

Weighted from the alarm log alone, before any trend data is read. The trend check never changes this number.

active duration0.96+0.240
priority0.50+0.175
alarm count0.40+0.100
repeat count0.00+0.000
nuisance category0.00-0.000

Trend rules, first to fire sets the band

  1. R-D1Trend within deadband demotedid not firepeak overshoot 2.25 deadbands exceeds the 1.00 allowed
  2. R-D2Excursion below minimum duration demotedid not firetotal 43200 s beyond the limit meets the 600 s minimum
  3. R-D3Self recovered and stable demotedid not firestill beyond the limit at the end of the segment
  4. R-P1Sustained excursion promotedid not firebeyond the limit for 50.0 percent of the segment, under the 60.0 percent required
  5. R-P2Monotonic drift promotedid not firereverses by 8.00 deadbands, more than the 1.00 allowed for a monotonic drift
  6. R-P3Excursion magnitude promotedid not firepeak deviation 2.25 deadbands is under the 4.00 required
Your call

Set aside as nuisance

Kept so every demotion can be checked. Each row names the rule that decided it.

PointTrendWhy it was set aside
SYN1/AHU-21-B/CLG_SPrepeating, 67 events, score 0.466
alarm-side class

No trend rule fired, so the repeating classification from the alarm log stands.

SYN1/AHU-21-B/MIXED_AIR_TEMPstale, 1 event, score 0.250
R-D3 Self recovered and stable

returned to normal and held 57600 s at no closer than 5.75 deadbands inside the limit, so the condition cleared itself and stayed clear

SYN1/AHU-21-B/MIXED_AIR_TEMPnone, 1 event, score 0.175
R-D3 Self recovered and stable

returned to normal and held 57600 s at no closer than 5.75 deadbands inside the limit, so the condition cleared itself and stayed clear

SYN1/AHU-21-B/HTG_DUCT_TEMPfleeting, 2 events, score 0.089
R-D2 Excursion below minimum duration

total 180 s beyond the limit is under the 600 s minimum, a fleeting excursion with no dwell behind it

Could not judge

No trend evidence to stand on, so the agent did not guess.

  • SYN1/AHU-21-B/HTG_DMPR_POS8 events, alarm-side score 0.271

    no trend series for this point in the trend export

Your marks

Mark each escalation above. Copy the result into the run's feedback folder. Marks stay in this browser only.

The corpus is synthetic, generated in the shape of the LBNL Fault Detection and Diagnostics datasets. These results show the pipeline and the scoring work end to end. They are not yet evidence that the approach transfers to a real building's alarm queue. Every threshold and weight lives in a config file changed by a person. The agent never tunes itself.

Source code, tests, and the build guide: github.com/AddisonTech/bms-alarm-triage-agent