13 KiB
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:
- Instant order creation: All 4 orders are created the moment sepsis is detected — no manual recall needed
- Deadline tracking: The 1-hour clock starts automatically
- Compliance monitoring: A background service checks every 5 minutes for overdue bundles
- 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:
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:
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:
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:
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