How to configure SLA monitoring¶
When you need this: You want first-class evidence for the two SLAs every AML examiner asks about: "how long do open alerts sit?" and "how fresh is the data the rules ran against?" You want this declared in the spec (peer-reviewed, replayable) rather than tracked in a side spreadsheet.
Prereqs:
Program.slablock onaml.yaml. Shipped in PR-LF1 (#415) / Round 27.Time: ~5 min to declare. Ongoing monitoring via reading
sla_report.jsondirectly — no dashboard page ornotify-digestflow currently consumes the SLA artifact (see §4 below).
The Pillar-6 SLA monitor (engine/sla.py) evaluates two breach types per run and emits sla_report.json — one of the eight manifest-hashed artifacts, so the SLA evidence is part of the immutable audit ledger. The monitor is observational, not enforcing: the engine never aborts on a breach; it records it for downstream surfaces.
Steps¶
1 · Declare the SLA block¶
program:
name: community_bank_aml
jurisdiction: US
regulator: FinCEN
owner: chief_compliance_officer
effective_date: 2026-01-01
sla:
alert_disposition_days: 30 # default 30; days before an open alert is "stale"
batch_cadence_days: 1 # default 1; expected cadence of the data extract
batch_lateness_grace_days: 1 # default 1; grace before late batch flagged
All three sub-fields are optional with the defaults shown. Omitting the whole sla: block disables the monitor (the report still emits, with enabled: false).
2 · Tune for your operational reality¶
| Field | Typical value | Why |
|---|---|---|
alert_disposition_days |
30 (FinCEN guidance) for high-volume programs, 14 for fintechs | The threshold above which open alerts become an exam finding |
batch_cadence_days |
1 for batch programs, 0 for near-real-time | Sets the "expected" gap between as_of and the most recent transaction |
batch_lateness_grace_days |
1 (24h) for batch, 0 for SLA-critical pipelines | Late-but-acceptable buffer before the run is flagged |
3 · Run + read the report¶
Output shape (fields from engine/sla.py::SLAReport):
{
"enabled": true,
"alert_disposition_days": 30,
"batch_cadence_days": 1,
"batch_lateness_grace_days": 1,
"total_breaches": 1,
"breaches_by_rule": {"structuring_burst": 1},
"breaches": [
{"case_id": "C0042-...", "opened_at": "2026-04-10T...", "age_days": 45, "rule_id": "structuring_burst"}
],
"batch_lateness_days": 2,
"batch_late": true,
"latest_transaction_at": "2026-05-23T18:42:00Z"
}
4 · Visualize over time¶
The SLA artifact is emitted to disk only today — sla_report.json per run, manifest-hash pinned, but no dashboard page currently subscribes to it. The Today landing card pack for Manager personas has a _sla_breach_count() helper that reads an sla_state column off df_cases, but the default DataFrame loader doesn't populate that column from program.sla / sla_report.json — so unless a custom loader enriches it, the breach count renders as 0 regardless of what the run actually emitted. For trend visualization you currently chart externally; a future dashboard surface that reads sla_report.json per archived run is the natural follow-up.
5 · Wire into ops escalation¶
The framework records the breach; routing the human escalation is yours. Common patterns:
- Read
sla_report.jsonfrom a downstream CI job; open a Linear/Jira ticket when eithertotal_breaches > 0(stale open alerts) orbatch_late == true(late data extract). The two metrics are independent — a late batch with no stale cases setsbatch_latetrue but leavestotal_breachesat 0, so a single-field trigger would silently skip pipeline lateness. This OR-trigger is the recommended integration today. aml notify-digest(CLI) generates a Markdown run summary — note that it readsdecisions.jsonlevents, notsla_report.json, so it doesn't currently surface SLA breaches in the digest. Wiring ansla_breachdecision-event emitter alongside the JSON report would surface it; until then, treat the artifact as the source of truth.- The post-run
monitoring_digest.json(Pillar-2 sibling, PR-LF4) carries the per-rule alert count delta — combine with the SLA report for "rules accumulating backlog."
Verify it worked¶
Four checks:
sla_report.jsonexists andenabled: truewhen the spec declares asla:block.manifest.json::sla_report_hashmatchessha256(sla_report.json).aml verify-decisionsonly checks thedecisions.jsonlchain — to verify the SLA artifact hash, recompute manually:sha256sum sla_report.jsonand compare tomanifest.json::sla_report_hash.- A planted late case (an open
case_openedevent older thanalert_disposition_daysand no terminal decision) appears inbreaches. - A planted late batch (
as_of - latest_transaction_at > cadence + grace) flipsbatch_late: true.
Common problems¶
| Symptom | Cause | Fix |
|---|---|---|
Report exists but enabled: false |
Program.sla block omitted |
Add the block (the report is always emitted; only the monitor needs explicit enablement) |
age_days looks off by 1 |
Full-timedelta vs .days-floor (resolved PR-LF1 codex pass-2) |
Re-run on R27+; the floor is now correct |
Late case_opened event ignored |
The opened-at timestamp has no timezone suffix | The parser handles Z/+00:00/naive (PR-LF1 codex pass-2). If still off, check that the event's timestamp ISO-formats correctly |
Cases with terminal events AFTER as_of still flagged as open |
Pre-PR-LF1 codex pass-3 bug | Re-run on R27+; the comparison now respects as_of strictly |
| Batch never flagged late | as_of overridden via --as-of to a recent date |
Expected. The check uses the run's as_of, not wallclock |
Next steps¶
- How to triage defects — Pillar-2 ticket queue (
defect_log.jsonl) for the sibling evidence: not "is alert stale?" but "did the data pipeline cough up something the engine couldn't process?" - Note:
pages/50_Drift_Monitor.pyplotspython_refalert-volume drift, NOT SLA breach trends. Adding an SLA-trend chart is a small follow-up that would consumesla_report.jsonper archived run. - The audit-evidence reference explains where
sla_report.jsonsits in the eight manifest-hashed artifacts.