321 lines
13 KiB
Markdown
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
|