Files
vigilcare-clinical/docs/guides/23-sepsis-bundle-automation.md
T

321 lines
13 KiB
Markdown

# Guide 23: Sepsis Bundle Automation
## What is a Sepsis Bundle?
**Sepsis** is a life-threatening condition where the body's response to an infection damages its own organs. It's one of the leading causes of death in hospitals, and early treatment dramatically improves survival. The **Surviving Sepsis Campaign** defines a set of mandatory interventions (a "bundle") that must be completed within 1 hour of sepsis recognition:
| Element | What It Is | Why It's Urgent |
|---------|-----------|----------------|
| Blood cultures | Draw blood samples before antibiotics | Identifies the infecting organism so treatment can be targeted |
| Serum lactate | Blood test for lactate level | High lactate indicates tissue damage from inadequate blood flow |
| Broad-spectrum antibiotics | Administer antibiotics immediately | Every hour of delay increases mortality by ~8% |
| IV fluid resuscitation | Administer 30 mL/kg crystalloid fluids | Restores blood volume and organ perfusion |
**What is a "bundle" in software terms?** It's a checklist of 4 orders that the system creates automatically when sepsis is detected. Each element is tracked as PENDING → COMPLETED, and the bundle as a whole is tracked as IN_PROGRESS → COMPLIANT or NON_COMPLIANT based on whether all 4 elements are completed within the 1-hour deadline.
---
## Why Automate Sepsis Bundles?
Without automation, a nurse sees a sepsis alert, mentally recalls the 4-element bundle, manually creates each order, and tracks compliance on paper. In a busy ICU with multiple deteriorating patients, elements get missed or delayed. Automation ensures:
1. **Instant order creation**: All 4 orders are created the moment sepsis is detected — no manual recall needed
2. **Deadline tracking**: The 1-hour clock starts automatically
3. **Compliance monitoring**: A background service checks every 5 minutes for overdue bundles
4. **Audit trail**: Every bundle is recorded with its triggering alert, deadline, and outcome
---
## Architecture Overview
```
SOFA score computed (delta >= 2 from baseline)
SofaDetector creates SOFA_SEPSIS alert
SepsisAlertHandler.OnSepsisAlertCreatedAsync()
SepsisBundleService.TryCreateBundleAsync()
├── Creates SepsisBundle (IN_PROGRESS, deadline = now + 1 hour)
├── Creates 4 SepsisBundleElements (PENDING)
├── Creates 4 Orders (orderedBy: "sepsis-bundle-engine")
└── All in one PostgreSQL transaction (atomic)
... 1 hour passes ...
SepsisBundleMonitorService (every 5 minutes)
├── Finds IN_PROGRESS bundles past deadline
└── Marks as NON_COMPLIANT if elements remain PENDING
```
---
## Trigger: SOFA Delta >= 2
Sepsis bundles are only triggered by `SOFA_SEPSIS` alerts — not by qSOFA screens, NEWS2 scores, or any other alert type:
```csharp
public class SepsisAlertHandler
{
public async Task OnSepsisAlertCreatedAsync(
Guid encounterId, Guid alertId, AlertType alertType, CancellationToken ct)
{
if (alertType != AlertType.SofaSepsis)
return; // Only SOFA_SEPSIS triggers a bundle
var bundle = await _bundleService.TryCreateBundleAsync(
encounterId, alertId, alertType, ct);
if (bundle is not null)
_logger.LogInformation(
"Sepsis bundle {BundleId} created for encounter {EncounterId}",
bundle.Id, encounterId);
}
}
```
**Why only SOFA_SEPSIS?** The Sepsis-3 definition requires evidence of organ dysfunction (SOFA delta >= 2 from baseline). A qSOFA screen (>= 2 criteria) is a bedside screen that recommends ordering SOFA labs — it doesn't confirm sepsis. Creating bundles on qSOFA would produce false positives. The clinical flow is: qSOFA screen → order labs → SOFA computed → if delta >= 2 → sepsis bundle.
---
## Bundle Creation: Atomic Transaction
The bundle, its 4 elements, and the 4 corresponding orders are all created in a single PostgreSQL transaction:
```csharp
public async Task<SepsisBundle?> TryCreateBundleAsync(
Guid encounterId, Guid alertId, AlertType alertType, CancellationToken ct)
{
// Idempotency: only one in-progress bundle per encounter
var existing = await _db.SepsisBundles
.AnyAsync(b => b.EncounterId == encounterId
&& b.ComplianceStatus == SepsisBundleComplianceStatus.InProgress, ct);
if (existing) return null;
await using var tx = await _db.Database.BeginTransactionAsync(ct);
var recognizedAt = DateTimeOffset.UtcNow;
var bundle = new SepsisBundle
{
Id = Guid.NewGuid(),
EncounterId = encounterId,
TriggeringAlertId = alertId,
TriggeringAlertType = alertType.ToDbString(),
RecognizedAt = recognizedAt,
DeadlineAt = recognizedAt.AddHours(1), // 1-hour compliance window
ComplianceStatus = SepsisBundleComplianceStatus.InProgress,
};
_db.SepsisBundles.Add(bundle);
// Create the 4 bundle elements with linked orders
var elements = new[]
{
("BLOOD_CULTURE", "Draw blood cultures (2 sets, aerobic + anaerobic)"),
("SERUM_LACTATE", "Obtain serum lactate level"),
("ANTIBIOTICS", "Administer broad-spectrum antibiotics"),
("IV_FLUIDS", "Begin IV crystalloid fluid resuscitation (30 mL/kg)"),
};
foreach (var (code, description) in elements)
{
var order = new Order
{
Id = Guid.NewGuid(),
EncounterId = encounterId,
OrderType = MapOrderType(code),
Description = description,
Status = OrderStatus.Pending,
OrderedBy = "sepsis-bundle-engine",
OrderedAt = recognizedAt,
};
_db.Orders.Add(order);
_db.SepsisBundleElements.Add(new SepsisBundleElement
{
Id = Guid.NewGuid(),
BundleId = bundle.Id,
ElementCode = code,
OrderId = order.Id,
Status = SepsisBundleElementStatus.Pending,
});
}
// Outbox events for downstream notification
_db.OutboxEvents.Add(/* sepsis.bundle.created event */);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);
_metrics.SepsisBundleComplianceTotal.WithLabels("CREATED").Inc();
return bundle;
}
```
**Why atomic?** If the bundle is created but one of the orders fails, you'd have a partially-created bundle with missing elements — clinicians would see a checklist with items missing. The transaction ensures all-or-nothing: either all 4 elements and their orders exist, or none do.
**Why `orderedBy: "sepsis-bundle-engine"`?** This identifies auto-created orders vs manually-created ones. Clinicians see that the order was system-generated and can distinguish it from orders they placed themselves.
---
## Bundle Element Lifecycle
Each bundle element starts as PENDING and moves to COMPLETED when the linked order is resulted:
```
PENDING ──(order resulted)──► COMPLETED
```
When a clinician marks an order as "resulted" (e.g., blood cultures drawn, antibiotics administered), the corresponding bundle element is updated. When all 4 elements are COMPLETED before the deadline, the bundle transitions:
```
IN_PROGRESS ──(all 4 elements completed within 1 hour)──► COMPLIANT
```
---
## Compliance Monitoring: SepsisBundleMonitorService
A background service runs every 5 minutes and checks for overdue bundles:
```csharp
public class SepsisBundleMonitorService : BackgroundService
{
private static readonly TimeSpan ScanInterval = TimeSpan.FromMinutes(5);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try { await ScanOverdueBundlesAsync(stoppingToken); }
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Sepsis bundle monitor error — will retry");
}
await Task.Delay(ScanInterval, stoppingToken);
}
}
internal async Task ScanOverdueBundlesAsync(CancellationToken ct)
{
var overdue = await db.SepsisBundles
.Where(b => b.ComplianceStatus == SepsisBundleComplianceStatus.InProgress
&& b.DeadlineAt < DateTimeOffset.UtcNow)
.ToListAsync(ct);
foreach (var bundle in overdue)
{
bundle.ComplianceStatus = SepsisBundleComplianceStatus.NonCompliant;
_metrics.SepsisBundleComplianceTotal
.WithLabels("NON_COMPLIANT").Inc();
var incompleteCount = await db.SepsisBundleElements
.CountAsync(e => e.BundleId == bundle.Id
&& e.Status != SepsisBundleElementStatus.Completed, ct);
_logger.LogWarning(
"Sepsis bundle {BundleId} for encounter {EncounterId} marked NON_COMPLIANT — " +
"deadline {Deadline} passed with {Incomplete} incomplete elements",
bundle.Id, bundle.EncounterId, bundle.DeadlineAt, incompleteCount);
}
if (overdue.Count > 0)
await db.SaveChangesAsync(ct);
}
}
```
The scan finds all bundles that are still IN_PROGRESS but past their deadline, marks them NON_COMPLIANT, and logs which elements were incomplete. The `sepsis_bundle_compliance_total` Prometheus counter tracks compliance outcomes on the Grafana dashboard.
---
## Idempotency: One Bundle Per Encounter
The `TryCreateBundleAsync` method checks for existing in-progress bundles before creating a new one:
```csharp
var existing = await _db.SepsisBundles
.AnyAsync(b => b.EncounterId == encounterId
&& b.ComplianceStatus == SepsisBundleComplianceStatus.InProgress, ct);
if (existing) return null;
```
This prevents multiple bundles from being created if the SOFA score triggers multiple `SOFA_SEPSIS` alerts (e.g., if the score worsens further). Only one bundle can be in progress per encounter at a time.
---
## The Complete Sepsis Detection Timeline
```
t=0:00 qSOFA screen: resp_rate=24, systolic_bp=95 (2/3 criteria)
→ QSOFA_SCREEN warning alert
→ Recommendation: "Order SOFA labs"
t=0:30 Labs drawn: platelets, bilirubin, creatinine
t=1:00 Lab results arrive + vitals recorded
→ SOFA baseline established (total = 3)
t=2:00 Patient deteriorates — new labs + vitals
→ SOFA current = 6, delta = 3 from baseline
→ SOFA_SEPSIS critical alert created
→ SepsisAlertHandler triggers bundle creation
t=2:00 Sepsis bundle created (deadline = t=3:00):
✓ Blood cultures order (PENDING)
✓ Serum lactate order (PENDING)
✓ Antibiotics order (PENDING)
✓ IV fluids order (PENDING)
t=2:10 Nurse draws blood cultures → order resulted → element COMPLETED (1/4)
t=2:15 Lactate result arrives → element COMPLETED (2/4)
t=2:20 Antibiotics administered → element COMPLETED (3/4)
t=2:35 IV fluids initiated → element COMPLETED (4/4)
→ Bundle status: COMPLIANT (within 1-hour deadline)
-- OR --
t=3:00 Deadline passes with 2/4 elements still PENDING
→ SepsisBundleMonitorService marks: NON_COMPLIANT
→ Prometheus counter: sepsis_bundle_compliance_total{status="NON_COMPLIANT"}
```
---
## Database Schema
```
sepsis_bundles
├── id (UUID)
├── encounter_id (FK)
├── triggering_alert_id (FK)
├── triggering_alert_type ("SOFA_SEPSIS")
├── recognized_at (timestamp)
├── deadline_at (recognized_at + 1 hour)
├── compliance_status ("IN_PROGRESS" | "COMPLIANT" | "NON_COMPLIANT")
└── completed_at (nullable)
sepsis_bundle_elements
├── id (UUID)
├── bundle_id (FK → sepsis_bundles)
├── element_code ("BLOOD_CULTURE" | "SERUM_LACTATE" | "ANTIBIOTICS" | "IV_FLUIDS")
├── order_id (FK → orders)
└── status ("PENDING" | "COMPLETED")
```
---
## Key Takeaways
- **Only SOFA_SEPSIS triggers bundles** — qSOFA is a screen, not a confirmation. Bundles require evidence of organ dysfunction (SOFA delta >= 2).
- **Atomic creation ensures completeness** — all 4 elements and orders are created in one transaction; no partially-created bundles
- **1-hour compliance window is automatically enforced** — the deadline is set at creation time and checked every 5 minutes
- **One bundle per encounter at a time** — prevents duplicate bundles from repeated SOFA alerts during deterioration
- **Auto-created orders are labeled** — `orderedBy: "sepsis-bundle-engine"` distinguishes automated from manual orders
- **Compliance is tracked as a Prometheus metric** — trends in COMPLIANT vs NON_COMPLIANT rates are visible on the dashboard for quality improvement