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

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:

  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:

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 labeledorderedBy: "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