medOS ultra

Treatment Series Screening Gate

Reusable multi-session pattern: plan + per-session screening + 3-outcome gate + auto-handoff, across 17 domains.

14 min read diagramsUpdated 2026-05-17docs/architecture/treatment-series-screening-gate-pattern.md

Status: design — extracted from the radiation therapy → procedure queue integration doc, generalized as a reusable pattern across all multi-session treatment domains in medOS-ultra.

Related docs:


TL;DR — One pattern, many domains

Many clinical workflows look identical at the abstract level:

A multi-session treatment plan is signed once → generates per-session scheduled visits → each visit goes through a clinician screening gate with configurable rules → outcome is 3-way: auto-pass / md-review / defer → on clearance, a delivery record is auto-created in the operational queue → defer flags scheduler, never auto-shifts.

This pattern applies to ≥16 clinical domains in medOS-ultra. Instead of building 16 bespoke flows, build the abstraction once as a treatment-series-engine package, then bind it per domain via configuration + a thin domain-specific screening form.


The 16 domains

Already partially modelled (extend existing)

# Domain Existing miniapp Existing entity Notes
1 Radiation therapy radiation-oncology/* (7 sub-modules), order-radiation-therapy-mock RadiationOncology + radiationOncologyHistory[] Has fractionNumber/totalFractions in frontend store; reference implementation
2 Chemotherapy oncology-emr Oncology + oncologyHistory[] Per-cycle screening (CBC nadir, weight, performance status). Nearly identical shape to RT.
3 Hemodialysis nephrology-clinic/nephrology-hemodialysis HemodialysisSessionData (frontend only) Has preWeight/postWeight/preVitals/postVitals per session. Backend schema missing.
4 Physical/occupational therapy rehabilitation-plan, rehabilitation-plans RehabPlan + TrainingLog[] Has frequency enum (daily / 3x_week / weekly), pain/fatigue/mood per session. Closest existing template.
5 Vaccination series / well-child pink-book-vaccine TreatmentPlanType.IMMUNIZATION Schedule-driven multi-visit; less gating today.
6 Transfusion series transfusion-reaction-notify, blood bank modules (multiple) Partial — reaction-notify exists, multi-transfusion protocol orchestration missing.

Net-new domains (no module today)

# Domain Key gate criteria Defer reason examples
7 Cardiac rehab (12wk × 3/wk) resting HR/BP, exercise tolerance, ECG check chest pain, BP >180, arrhythmia
8 Pulmonary rehab SpO2, dyspnea score (mMRC), recent exacerbation desat <88%, infection
9 IVF cycles (protocol-driven) estradiol, follicle count, lining thickness OHSS risk, poor response
10 TB DOT (6–9 months daily) AST/ALT, vision, hearing, weight hepatotoxicity, optic neuritis
11 Methadone/Buprenorphine MAT UDS, pregnancy, behavior screen aberrant UDS, sedation
12 Allergy immunotherapy prior reaction grade, recent illness, peak flow grade ≥2 reaction, asthma flare
13 Anticoagulation (warfarin) INR, bleeding screen, med interactions INR >5, recent bleed, drug interaction
14 Pain clinic series functional improvement, UDS, aberrant behavior UDS inconsistent, lost rx
15 Antenatal care (ANC) visits BP, urine, fundal height, fetal HR, milestone labs proteinuria, growth restriction
16 Wound care series wound size, exudate, infection signs, healing rate infection, no healing 4wk
17 Long-acting psych injections mental status, weight, EPS, prolactin severe EPS, weight gain >7%

That’s 17 candidates. The pattern fits all of them with minor field variations.


The unifying abstraction

Every domain instance of this pattern has these 7 parts:

┌─────────────────────────────────────────────────────────────────────┐
│                     TREATMENT-SERIES ENGINE                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. PLAN ENTITY                                                      │
│     ├─ patientRef, encounterRef, doctorRef                          │
│     ├─ totalSessions, frequency, startDate                          │
│     ├─ gateRules (jsonb — configured at plan signing)               │
│     └─ baselineMeasurements (weight, labs, vitals, scores)          │
│                                                                      │
│  2. SESSION GENERATOR                                                │
│     ├─ generateScheduleDates(start, total, frequency, calendar)     │
│     ├─ creates N sessionRequest rows (= consultRequest in RT)       │
│     └─ caches gateRules + plan metadata onto each row               │
│                                                                      │
│  3. CHECK-IN                                                         │
│     ├─ scheduled → checked-in (QR scan, staff scan, kiosk)         │
│     └─ surfaces in nurse/clinician screening queue                  │
│                                                                      │
│  4. SCREENING FORM                                                   │
│     ├─ vitals (BP/HR/SpO2/temp/weight)                              │
│     ├─ domain-specific fields (toxicity grades / labs / scores)     │
│     ├─ nurse notes + escalation override                            │
│     └─ live gate preview                                            │
│                                                                      │
│  5. GATE EVALUATOR (pure function)                                   │
│     ├─ evaluateGate(payload, rules, context) → outcome              │
│     ├─ outcomes: auto-pass | md-review | (defer is human-only)      │
│     └─ same code runs in frontend preview + backend authoritative   │
│                                                                      │
│  6. MD REVIEW QUEUE                                                  │
│     ├─ clinician sees screening data + trend + history              │
│     └─ decision: clear / modify (with annotation) / defer           │
│                                                                      │
│  7. DELIVERY HANDOFF                                                 │
│     ├─ on clearance: auto-create delivery record (procedureRequest, │
│     │  medicationAdmin, dialysisSession, etc.) in target queue      │
│     ├─ on defer: emit event to scheduler queue (no auto-shift)      │
│     └─ delivery completion closes the loop back to plan progress    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Where it actually exists in the codebase TODAY

Component Where today Reusability
Plan entity (generic) packages/platform-api-schema/src/medication/treatmentPlan/entity/TreatmentPlan.tsTreatmentPlan with data[] history array ✅ Reuse as base; extend with sessionCount, frequency, gateRules, baseline* fields
Plan entity (RT-specific) radiationOncology entity ✅ Extend with gateRules, baselineWeightKg, baselineLabs
Plan entity (chemo-specific) oncology entity ✅ Same extensions
Session record (RT) TreatmentSession interface in web/src/containers/radiation-oncology/store.ts ✅ Best existing template — has sessionNumber, fractionNumber, totalFractions, status
Session record (rehab) TrainingLog in rehabilitation-plans/types.ts ✅ Has per-session metrics (painLevel, fatigueLevel, completionRate)
Session record (dialysis) HemodialysisSessionData (frontend only) ⚠️ Needs backend schema lift
Per-visit screening PreScreening entity — has vitalSign[], screeningInfo, fast-track flags ✅ Closest gate precedent — extend for per-session use
Vital signs capture VitalSign entity — per-encounter, structured vitalSignComponents[] ✅ Reuse directly
Scheduling primitives Appointment entity ⚠️ Designed for one-off; doesn’t link sessions to series
Workflow JSONs 23 templates in packages/medical-kit/src/medical-worklist/defaults/ ⚠️ All single-session focused — need a multi-session-treatment-workflow.json template
Order/billing wrapper orderRequest ✅ Reuse as-is — already proven for RT (OrderRadiationTherapyMock)
Procedure queue UI web/packages/medical-kit/src/procedure/components/procedure-queue/ ✅ Reuse with category filter (Phase 4 in RT doc)
Acknowledgement system AcknowledgementRequest (see acknowledgement-system.md) ✅ Use for MD review notifications
Policy gates policy_gates table (see policy-gates.md) ✅ Adjacent — same admin UI, action-level instead of session-level
QR self-service services/public-api/.../patientMenu/ pattern ✅ Reuse for per-session check-in tokens
Cron registry cron_jobs table ✅ For scheduled gates (e.g. “every Monday MD review”)

Proposed reusable package layout

packages/medical-kit/src/treatment-series-engine/
├── types.ts                          # Shared types (GateRules, ScreeningPayload, GateOutcome)
├── evaluateGate.ts                   # Pure function — same code runs FE + BE
├── gatePresets/                      # Domain-specific gate templates
│   ├── radiationOncologyPresets.ts   # standard / sbrt / brachy / palliative
│   ├── chemotherapyPresets.ts        # FOLFOX / FOLFIRI / paclitaxel / pembro
│   ├── hemodialysisPresets.ts        # standard / hd-acute / hdf
│   ├── rehabPresets.ts               # pt / ot / cardiac-rehab / pulmonary-rehab
│   ├── ivfPresets.ts                 # long-protocol / antagonist / mild
│   ├── tbDotPresets.ts               # HRZE / continuation phase
│   ├── matPresets.ts                 # methadone / buprenorphine
│   ├── allergyItPresets.ts           # build-up / maintenance / cluster
│   ├── anticoagulationPresets.ts     # warfarin-target-2-3 / target-2.5-3.5
│   ├── ancPresets.ts                 # low-risk / high-risk pregnancy
│   ├── woundCarePresets.ts           # diabetic-foot / pressure-ulcer / venous
│   └── psychInjectionPresets.ts      # paliperidone / aripiprazole / risperidone
│
├── scheduleGenerator/
│   ├── generateScheduleDates.ts      # Frequency-aware (daily/3x/wkly/q21d/q28d)
│   ├── frequencyTypes.ts             # 'daily' | '3x_week' | 'weekly' | 'q14d' | 'q21d' | 'q28d' | 'custom'
│   └── holidayCalendar.ts            # Optional clinic_holidays integration
│
├── sessionFactory/
│   ├── createSessionRequests.ts      # Generic per-session row generator (= consultRequest in RT)
│   └── attachPlanMetadata.ts         # Caches gateRules + plan refs onto each session
│
├── screening/
│   ├── ScreeningFormShell.tsx        # Container — renders vitals + domain-specific fields + live preview
│   ├── VitalsBlock.tsx               # Always-visible BP/HR/SpO2/temp/weight
│   ├── GateOutcomePreview.tsx        # Live evaluator chip
│   └── slots/                        # Domain-specific field components plug into here
│       ├── ToxicityGrid.tsx          # CTCAE — RT, chemo, IT
│       ├── DialysisAccess.tsx        # HD-specific
│       ├── PainFunctionScale.tsx     # PT/OT, pain clinic
│       ├── FollicleScan.tsx          # IVF
│       ├── LiverFunctionPanel.tsx    # TB, anticoag
│       ├── INRPanel.tsx              # Anticoag
│       ├── FundalHeightFetalHr.tsx   # ANC
│       └── WoundMeasurement.tsx      # Wound care
│
├── mdReview/
│   ├── MdReviewQueue.tsx             # Generic flagged-cases list
│   ├── MdReviewDialog.tsx            # Generic clear/modify/defer dialog
│   └── TrendSparkline.tsx            # Reusable visual of any metric across sessions
│
├── deliveryHandoff/
│   ├── handoffRegistry.ts            # Maps planType → delivery entity (procedureRequest, medicationAdmin, etc.)
│   └── createDeliveryRecord.ts       # Auto-create on clearance
│
├── deferQueue/
│   ├── DeferRescheduleQueue.tsx      # Generic "needs reschedule" task list
│   └── insertMakeupSession.ts        # Append-only, no auto-shift
│
└── qr/
    ├── mintSeriesToken.ts            # Token scoped to a plan + array of session IDs
    └── verifyAndCheckIn.ts           # Public route handler

Common types

// types.ts
export type Frequency = 'daily' | '3x_week' | 'weekly' | 'q14d' | 'q21d' | 'q28d' | 'custom';

export type GateOutcome =
  | { outcome: 'auto-pass' }
  | { outcome: 'md-review'; reasons: string[] }
  | { outcome: 'defer'; reasons: string[]; deferredBy: 'nurse' | 'md' };

export type GateRules = {
  // Universal vitals
  weightLossPctFromBaseline?: number;
  systolicBpMaxMmHg?: number;
  systolicBpMinMmHg?: number;
  heartRateMaxBpm?: number;
  heartRateMinBpm?: number;
  spO2MinPct?: number;
  tempMaxC?: number;

  // Schedule-based mandatory review
  mandatoryReviewSessions?: number[];      // e.g. [10, 20, 30]
  mandatoryReviewDayOfWeek?: number[];     // e.g. [1] = every Mon
  mandatoryReviewWeeks?: number[];         // e.g. [3, 5]

  // Pre-clearance ranges
  preClearedSessions?: [number, number][]; // [[1,5]] = auto-pass sessions 1-5

  // Domain-specific extensions go in `domain` field
  domain?: Record<string, unknown>;        // e.g. { anc: 1.5, plt: 100 } for chemo/RT
};

export type ScreeningPayload = {
  // Universal vitals
  weightKg?: number;
  weightDeltaPct?: number;
  bpSystolic?: number;
  bpDiastolic?: number;
  heartRate?: number;
  spO2?: number;
  tempC?: number;
  painScore0to10?: number;

  // Universal escape hatches
  nurseNotes?: string;
  nurseEscalateToMd?: boolean;
  escalationReason?: string;

  // Domain-specific extensions
  domain?: Record<string, unknown>;        // toxicity[], anc, follicle counts, INR, etc.
};

export type SessionContext = {
  sessionNumber: number;
  totalSessions: number;
  courseWeek: number;
  dayOfWeek: number;
  baseline?: Record<string, number>;       // weightKg, labs, etc.
};

Domain-specific gate examples (illustrative)

Chemotherapy (FOLFOX cycle 4 of 12)

{
  "weightLossPctFromBaseline": 10,
  "mandatoryReviewSessions": [1, 4, 8, 12],
  "domain": {
    "ancMin": 1.5,
    "plateletMin": 100,
    "hemoglobinMin": 9,
    "creatinineMax": 1.5,
    "neuropathyMaxGrade": 2
  }
}

Hemodialysis (3x/week thrice)

{
  "domain": {
    "interdialyticWeightGainMaxKg": 3,
    "preBpSystolicMax": 200,
    "preBpSystolicMin": 90,
    "accessPatencyRequired": true,
    "potassiumMax": 6.0
  }
}

Cardiac rehab

{
  "heartRateMaxBpm": 140,
  "systolicBpMaxMmHg": 180,
  "domain": {
    "restingEcgRhythm": "sinus",
    "dyspneaScoreMax": 2,
    "chestPainNoneRequired": true
  }
}

IVF stimulation phase

{
  "mandatoryReviewSessions": [1, 3, 5, 7, 9],
  "domain": {
    "estradiolMax": 5000,
    "follicleCountMin": 3,
    "ohssRiskScoreMax": 3
  }
}

TB DOT (intensive phase)

{
  "mandatoryReviewSessions": [14, 30, 60],
  "domain": {
    "astMax": 120,
    "altMax": 120,
    "bilirubinMax": 2.5,
    "visualAcuityRequired": true
  }
}

Anticoagulation (target INR 2-3)

{
  "mandatoryReviewSessions": [1, 2, 3, 4],
  "domain": {
    "inrMin": 1.8,
    "inrMax": 3.5,
    "bleedingScreenRequired": true,
    "missedDosesAllowedMax": 1
  }
}

Antenatal care (low-risk pregnancy)

{
  "systolicBpMaxMmHg": 140,
  "mandatoryReviewSessions": [12, 20, 28, 32, 36, 38, 39, 40],
  "domain": {
    "proteinuriaMaxDipstick": 1,
    "fundalHeightTolerance": 3,
    "fetalHrMin": 110,
    "fetalHrMax": 160
  }
}

Delivery-record handoff per domain

Each domain has a different “delivery record” that gets auto-created on screening clearance:

Plan Session screening Delivery record on clearance Queue location
Radiation therapy consultRequest procedureRequest (category=radiation) /procedure/procedure-queue?dept=radiation
Chemotherapy consultRequest medicationAdministration /oncology/infusion-queue
Hemodialysis consultRequest dialysisSession /nephrology/dialysis-floor
Rehab (PT/OT) consultRequest trainingLog (existing pattern) /rehab/today
IVF consultRequest medicationOrder (gonadotropin dose) + monitoringScan /ivf/today
TB DOT consultRequest medicationAdministration (observed dose) /tb-clinic/today
Allergy IT consultRequest medicationAdministration (injection) /allergy-clinic/today
Anticoag consultRequest medicationOrder (warfarin dose) /anticoag-clinic/today
ANC consultRequest encounterCompletion (visit done) + next visit scheduled /anc/today
Wound care consultRequest procedureRequest (debridement/dressing) /wound-clinic/today
Psych injection consultRequest medicationAdministration /psych-clinic/today

The deliveryHandoff/handoffRegistry.ts maps planType → delivery entity factory.


Why this works as a single abstraction

What’s universal across all 17 domains:

✅ One plan signs off many sessions at a known cadence ✅ Each session needs a per-visit clinician check ✅ The check produces a 3-way outcome (proceed / escalate / defer) ✅ Configurable thresholds set at plan-signing time (clinical decision encoded once) ✅ MD has authority to modify or defer with annotation ✅ Defer flags scheduler; never silently mutates downstream sessions ✅ On clearance, a delivery record appears in the operational queue ✅ Patient can self-check-in via QR token scoped to their plan

What varies (and is handled by configuration / plug-in):

🔹 Domain-specific screening fields (slots into screening/slots/) 🔹 Threshold field names + units (in gateRules.domain) 🔹 Delivery record type (mapped in handoffRegistry) 🔹 Schedule cadence (Frequency enum + custom generator) 🔹 Worklist department / clinic routing (via clinicRef)


Implementation roadmap

Round 1 — Build for RT (proves the pattern)

Already speced in radiation-therapy-procedure-queue-integration.md. 6 phases. This is the reference implementation — every type, function, and UI component built here goes into the shared package as the second domain comes online.

Round 2 — Refactor into treatment-series-engine package

After RT phases 1–3 ship and stabilize (~2 weeks), pull the patterns out:

  • Generic evaluateGate, generateScheduleDates, MdReviewDialog, DeferRescheduleQueue move to packages/medical-kit/src/treatment-series-engine/
  • RT-specific code becomes a thin wrapper (radiationOncologyAdapter.ts)
  • All 4 RT gate presets move to gatePresets/radiationOncologyPresets.ts

Round 3 — Bind the next 2 domains (validates the abstraction)

Pick chemotherapy + hemodialysis — they have the most existing infrastructure to lift:

  • Chemo: 80% reuse — Oncology entity already shaped right, frontend has oncology-emr. Add gate rules + screening form slots.
  • HD: 60% reuse — HemodialysisSessionData frontend types exist, but backend schema needs first-class lift. Build it as a treatment-series-engine instance from day 1.

If both come together with <50% the effort of RT, the abstraction is validated.

Round 4 — Long tail

Add the remaining 12+ domains opportunistically as departments request them. Each new domain is ~3-5 days work once the engine exists: define the plan-type extension, set up gate presets, build screening slot components, wire handoff.


Migration safety

Don’t break existing domains during the abstraction step. Existing miniapps (oncology-emr, radiation-oncology/*, rehabilitation-plan, nephrology-hemodialysis) keep working unchanged. The engine is opt-in:

  1. Phase 1 of each domain migration = add adapter that calls the engine alongside existing code
  2. Phase 2 = the new screening + MD review flow becomes the recommended path, old UI stays for backward-compat
  3. Phase 3 (optional) = retire old UI once new flow is proven

No big-bang. No flag day.


Open questions

  1. One plan entity or one per domain?

    • Option A: extend the generic TreatmentPlan and store domain-type discriminator
    • Option B: keep RadiationOncology, Oncology, etc. as separate entities + extract shared mixin
    • Recommendation: B — keeps existing code paths working, mixin shares the gate-related fields
  2. Where does gateRules live for new domains?

    • On the plan entity itself (mirrored from RT)
    • Or on a separate screeningGatePolicy entity referenced by plan
    • Recommendation: on the plan — colocation makes the policy immutable per plan and easy to inspect
  3. Should this share infra with policy_gates?

    • policy_gates is action-level (this button is blocked because of rule X)
    • Screening gate is decision-level (this session goes to auto-pass/md-review/defer)
    • They’re complementary. Same admin UI is fine. Same evaluator engine — probably not, different semantics.
  4. Trend visualization shared component?

    • MD review dialog shows sparklines across sessions
    • Build once in treatment-series-engine/mdReview/TrendSparkline.tsx, takes metric: string + sessions: Session[]
    • All domains get free trend visualization
  5. Cross-plan concurrency (patient on chemo + RT simultaneously, common in cervix cancer)?

    • Different plans, different consultRequests, different MD review queues — handled by routing
    • One unified “today’s screening” worklist per nurse with parentPlanType color/badge to distinguish

Where to add this to CLAUDE.md

Add to the top-level index in CLAUDE.md (the # Key Files table at root):

| `docs/architecture/treatment-series-screening-gate-pattern.md` | Reusable multi-session treatment pattern — plan + per-session screening + 3-outcome gate + auto-handoff to operational queue. RT is reference impl; covers chemo / HD / rehab / IVF / TB DOT / MAT / anticoag / ANC / wound / psych injections / etc. |
Ask Anything