Handling Out-of-Range Flags Without Manual Intervention
Problem Statement
A static reference-range check flags every value outside its stored bounds, but most of those flags are not clinically actionable: a chronically elevated creatinine on a dialysis patient, a neonatal bilirubin that is normal for age, or a spuriously high potassium from a hemolyzed draw all trip the same limit. When every flag lands in a human review queue, throughput collapses and genuinely critical results wait behind noise. This page builds a deterministic pipeline that auto-resolves the resolvable flags — releasing chronically abnormal-but-stable results, holding results a calibration problem makes untrustworthy, and escalating true criticals immediately — while writing an attributable audit record for every automated decision so the laboratory director can defend each release.
Prerequisites
- Runtime: Python 3.11+ (for
X | Noneunions anddatetime.UTC). - Libraries:
pydantic>=2.6for typed flag, verdict, and audit models;pytestfor the verification suite. - Instrument firmware / middleware: analyzers must export a daily quality-control summary — control means, SDs, and the evaluated Westgard state per analyte — plus reagent-lot metadata, alongside each chemistry result.
- Regulatory baseline: the laboratory has a documented auto-verification (auto-release) procedure on file, a CLIA §493.1253 results-review requirement, and an append-only audit store as built in implementing HIPAA-compliant audit trails in LIMS.
- Upstream contract: results arrive already parsed and normalized to canonical analyte codes and UCUM units by the HL7 v2 Segment Mapping layer, with reference intervals and delta limits owned and version-controlled by the parent Threshold Tuning & Calibration stage. This code resolves flags; it never invents thresholds.
This build sits downstream of the Clinical Result Validation & Rule Engine Architecture runtime path — it consumes the flags raised by the stateless Reference Range Check Implementation and the verdicts produced by Delta Validation & Trend Analysis, then decides disposition.
Step-by-Step Implementation
Step 1: Model the flag and every possible disposition as typed data
Auto-resolution decisions are audited, so they cannot be loose dictionaries. Model the inbound flag and the terminal Disposition as frozen pydantic types. Every automated decision must reduce to exactly one disposition, so encode them as an enum rather than free strings.
from __future__ import annotations
from decimal import Decimal
from enum import Enum
from pydantic import BaseModel, Field
class Disposition(str, Enum):
AUTO_OVERRIDE = "auto_override" # release without human review
MANUAL_QUEUE = "manual_queue" # hold for a validation technologist
ALERT_IMMEDIATE = "alert_immediate" # page now; bypass the queue
HOLD_RECAL = "hold_recalibration" # QC out of control; do not release
class PatientMeta(BaseModel):
model_config = {"frozen": True}
sex: str | None = None
age_group: str | None = None # e.g. "neonate", "adult", "geriatric"
matrix: str = "serum"
class OutOfRangeFlag(BaseModel):
model_config = {"frozen": True}
result_id: str
patient_id: str
analyte: str # canonical, e.g. LOINC-resolved
value: Decimal
unit: str
patient_meta: PatientMeta
is_critical: bool = False # value past a hard critical limit
reagent_lot: str
Step 2: Re-resolve the reference range against patient demographics
A generic adult interval produces the largest share of false flags. Before treating a flag as real, re-resolve it against a demographic-keyed interval; many “out of range” results are in range for the patient’s age and sex. Resolve deterministically and fall back explicitly — a missing key is never silently treated as in range.
class Interval(BaseModel):
model_config = {"frozen": True}
low: Decimal
high: Decimal
class RangeResolution(BaseModel):
in_range: bool
used_default: bool
error: str | None = None
def resolve_range(
flag: OutOfRangeFlag, intervals: dict[str, Interval]
) -> RangeResolution:
m = flag.patient_meta
key = f"{flag.analyte}_{m.sex}_{m.age_group}_{m.matrix}"
bounds = intervals.get(key)
used_default = False
if bounds is None:
used_default = True
bounds = intervals.get(f"{flag.analyte}_default")
if bounds is None:
return RangeResolution(
in_range=False, used_default=True,
error=f"NO_RANGE_CONFIGURED:{flag.analyte}",
)
return RangeResolution(
in_range=bounds.low <= flag.value <= bounds.high,
used_default=used_default,
)
A used_default=True resolution is a confidence penalty, not a pass: it means the demographic-specific interval was missing, so the decision cannot be fully trusted for auto-release.
Step 3: Fold in the delta verdict to suppress stable chronic abnormality
A value that is out of range but unchanged from the patient’s own recent baseline is usually chronic, not acute. Consume the delta_status the Delta Validation & Trend Analysis stage already computed — this code does not recompute deltas — and use STABLE as a signal that a persistently abnormal result is safe to release.
class DeltaStatus(str, Enum):
STABLE = "stable"
TRENDING = "trending"
ACUTE_SHIFT = "acute_shift"
NO_BASELINE = "no_baseline"
def delta_confidence(status: DeltaStatus) -> Decimal:
"""Map an upstream delta verdict to a release-confidence contribution."""
return {
DeltaStatus.STABLE: Decimal("0.40"), # chronic, unchanged
DeltaStatus.TRENDING: Decimal("0.10"),
DeltaStatus.ACUTE_SHIFT: Decimal("0.00"), # never auto-release
DeltaStatus.NO_BASELINE: Decimal("0.15"),
}[status]
Step 4: Gate on the QC / calibration state with a circuit breaker
An out-of-range flag is only meaningful if the analyzer that produced it was in control. If the daily QC series has tripped a rejection rule — 1_3s, 2_2s, R_4s, 4_1s, or 10_x — no result from that analyte may auto-release, because the calibration governing the bound is itself suspect. And if the QC service is unreachable, fail closed to STRICT_MODE rather than assuming control.
| QC state | Meaning | Auto-resolution effect |
|---|---|---|
IN_CONTROL |
No Westgard rejection | Auto-release permitted |
WARNING (1_2s) |
Single control > 2 SD | Permitted; confidence penalty applied |
OUT_OF_CONTROL |
Rejection rule tripped | Force HOLD_RECAL; block release |
UNREACHABLE |
QC service timeout | Force STRICT_MODE; no auto-override |
class QCState(str, Enum):
IN_CONTROL = "in_control"
WARNING = "warning"
OUT_OF_CONTROL = "out_of_control"
UNREACHABLE = "unreachable"
def qc_gate(state: QCState) -> tuple[bool, Decimal]:
"""Return (auto_release_allowed, confidence_contribution)."""
if state in (QCState.OUT_OF_CONTROL, QCState.UNREACHABLE):
return False, Decimal("0.00")
penalty = Decimal("0.10") if state is QCState.WARNING else Decimal("0.00")
return True, Decimal("0.30") - penalty
The reagent-lot metadata on each flag matters here: a lot change shifts the analytical baseline, and the population feedback that retunes bounds against observed false-positive rates is owned by the Threshold Tuning & Calibration stage — this gate only enforces the current control state.
Step 5: Score confidence and route to a single disposition
Combine the signals into one confidence score, then map it to a disposition with an explicit auto-release floor. Criticals short-circuit to paging before any scoring; an out-of-control analyzer short-circuits to a recalibration hold. Everything else releases only above the confidence floor.
AUTO_RELEASE_FLOOR = Decimal("0.85")
def route_flag(
flag: OutOfRangeFlag,
rng: RangeResolution,
delta: DeltaStatus,
qc: QCState,
) -> tuple[Disposition, Decimal]:
if flag.is_critical:
return Disposition.ALERT_IMMEDIATE, Decimal("1.00")
if qc in (QCState.OUT_OF_CONTROL,):
return Disposition.HOLD_RECAL, Decimal("0.00")
# Demographic re-resolution can clear the flag outright.
if rng.error is None and rng.in_range and not rng.used_default:
return Disposition.AUTO_OVERRIDE, Decimal("0.95")
allowed, qc_conf = qc_gate(qc)
base = Decimal("0.15") if not rng.used_default else Decimal("0.00")
confidence = base + delta_confidence(delta) + qc_conf
if delta is DeltaStatus.ACUTE_SHIFT or rng.error is not None:
return Disposition.MANUAL_QUEUE, confidence
if allowed and confidence >= AUTO_RELEASE_FLOOR:
return Disposition.AUTO_OVERRIDE, confidence
return Disposition.MANUAL_QUEUE, confidence
The escalation of an ALERT_IMMEDIATE disposition is owned downstream by Critical Value Alert Routing, which consumes this verdict and dispatches the page; this stage only decides that a page is required.
Step 6: Emit an immutable audit record for every automated decision
An auto-released abnormal result must be as defensible as a human-verified one. Write an append-only audit record — hashing the patient identifier, capturing the full signal set and the resolving rule version — inside the same transactional boundary as the release, so no decision can exist without its provenance.
import hashlib
import uuid
from datetime import UTC, datetime
class ResolutionAudit(BaseModel):
model_config = {"frozen": True}
audit_id: str
result_id: str
patient_hash: str
analyte: str
disposition: Disposition
confidence: Decimal
delta_status: DeltaStatus
qc_state: QCState
reagent_lot: str
rule_version: str
decided_at: datetime
async def resolve_and_audit(
flag: OutOfRangeFlag,
delta: DeltaStatus,
qc: QCState,
intervals: dict[str, Interval],
audit_sink, # append-only store; awaited write
rule_version: str = "2026.07",
) -> ResolutionAudit:
rng = resolve_range(flag, intervals)
disposition, confidence = route_flag(flag, rng, delta, qc)
record = ResolutionAudit(
audit_id=str(uuid.uuid4()),
result_id=flag.result_id,
patient_hash=hashlib.sha256(flag.patient_id.encode()).hexdigest(),
analyte=flag.analyte,
disposition=disposition,
confidence=confidence,
delta_status=delta,
qc_state=qc,
reagent_lot=flag.reagent_lot,
rule_version=rule_version,
decided_at=datetime.now(UTC),
)
await audit_sink.append(record)
return record
Verification & Testing
Pin the three behaviors that carry the most clinical and regulatory weight: a stable chronic abnormality auto-releases, an out-of-control analyzer never does, and a critical value pages regardless of every other signal.
from decimal import Decimal
def _flag(**kw):
base = dict(
result_id="r1", patient_id="P1", analyte="CREA",
value=Decimal("2.4"), unit="mg/dL",
patient_meta=PatientMeta(sex="M", age_group="adult", matrix="serum"),
reagent_lot="LOT-A",
)
base.update(kw)
return OutOfRangeFlag(**base)
def test_stable_chronic_abnormal_auto_releases():
flag = _flag()
intervals = {"CREA_M_adult_serum": Interval(low=Decimal("0.7"),
high=Decimal("1.3"))}
rng = resolve_range(flag, intervals) # out of range, demographic-specific
disp, conf = route_flag(flag, rng, DeltaStatus.STABLE, QCState.IN_CONTROL)
assert disp is Disposition.AUTO_OVERRIDE
assert conf >= AUTO_RELEASE_FLOOR
def test_out_of_control_qc_forces_hold():
flag = _flag()
intervals = {"CREA_M_adult_serum": Interval(low=Decimal("0.7"),
high=Decimal("1.3"))}
rng = resolve_range(flag, intervals)
disp, _ = route_flag(flag, rng, DeltaStatus.STABLE, QCState.OUT_OF_CONTROL)
assert disp is Disposition.HOLD_RECAL
def test_critical_value_always_pages():
flag = _flag(value=Decimal("9.9"), is_critical=True)
rng = resolve_range(flag, {})
disp, _ = route_flag(flag, rng, DeltaStatus.ACUTE_SHIFT, QCState.OUT_OF_CONTROL)
assert disp is Disposition.ALERT_IMMEDIATE
Expected results: all three pass. Extend the suite with a used_default=True case (must route to MANUAL_QUEUE, since demographic confidence is missing), a QCState.UNREACHABLE case (must not auto-release), and a boundary case where combined confidence equals AUTO_RELEASE_FLOOR exactly (which releases, since the comparison is >=). A ResolutionAudit round-trip through model_dump_json() and back confirms the audit payload serializes losslessly for the append-only store.
Compliance Note
Auto-resolution of out-of-range flags is an auto-verification activity governed by CLIA §493.1253(b)(2), which requires the laboratory to establish, verify, and follow documented procedures for reviewing results before release. The explicit auto-release floor, the QC gate that blocks release from an out-of-control analyzer, and the forced hold on acute shifts are the auditable evidence that clinically significant results are not silently auto-released. Because each ResolutionAudit record is attributable, timestamped in UTC, hashed, and written immutably in the release transaction, the pipeline also satisfies the electronic-record requirements of 21 CFR Part 11.10(e) for a tamper-evident review trail. The patient_hash and the history reads that feed the delta verdict stay inside the CLIA/CAP data boundaries that segment protected patient data from downstream interfaces.
Troubleshooting
Chronically abnormal results are being queued for manual review instead of auto-releasing
The demographic-specific interval is probably missing, so resolve_range returns used_default=True and the confidence base drops to zero, pushing the score below the auto-release floor. Confirm the {analyte}_{sex}_{age_group}_{matrix} key exists in the interval store for this cohort; a _default fallback keeps the result safe but intentionally will not auto-release.
Everything from one analyzer suddenly stopped auto-releasing
Check its quality-control state. A tripped Westgard rejection rule sets QCState.OUT_OF_CONTROL, which forces HOLD_RECAL for every flag on that analyte until the control series recovers. If the QC state is UNREACHABLE, the circuit breaker has fallen back to strict mode because the QC service timed out — restore the QC feed and the analyzer returns to normal auto-resolution.
A genuinely critical value was auto-released instead of paging
route_flag short-circuits to ALERT_IMMEDIATE only when is_critical is set on the flag. If a critical slipped through, the hard critical-limit check that sets is_critical did not fire upstream — verify the critical limits in the Reference Range Check Implementation and that the flag carried the boolean into this stage.
Auto-override rates jumped after a reagent lot change
A new lot shifts the analytical baseline, so bounds tuned for the prior lot now over-release. The lot is captured on every flag and audit record; map the reagent lot to the effective interval version and let the population feedback in Threshold Tuning & Calibration recalibrate the bounds against the observed false-positive rate.
We cannot reconstruct why a specific result was auto-released
The ResolutionAudit record for that result_id holds the full decision context — disposition, confidence, delta status, QC state, reagent lot, and rule_version. If a record is missing, the audit write was outside the release transaction; move audit_sink.append inside the same transactional boundary as the release so a decision can never exist without its provenance.
Related
- Threshold Tuning & Calibration — the parent stage that owns the reference intervals, delta limits, and QC-driven recalibration this resolver consumes.
- Reference Range Check Implementation — the stateless static-limit stage that raises the out-of-range flags resolved here.
- Delta Validation & Trend Analysis — supplies the
delta_statusused to suppress stable chronic abnormality. - Implementing Delta Checks for Electrolyte Panels in Python — the sibling build whose electrolyte breaches feed into this auto-resolution decision.
- Critical Value Alert Routing — consumes the
ALERT_IMMEDIATEdisposition and dispatches the page.
Part of: Threshold Tuning & Calibration