Treatment Series Screening Gate
Reusable multi-session pattern: plan + per-session screening + 3-outcome gate + auto-handoff, across 17 domains.
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:
radiation-therapy-procedure-queue-integration.md— reference implementation (RT)procedure-day-queue.md— procedure queue infrastructurecentral-order-system.md— orderRequest billing hubacknowledgement-system.md— for MD review notificationspolicy-gates.md— adjacent pattern for action-level gating
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.ts — TreatmentPlan 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,DeferRescheduleQueuemove topackages/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 —
Oncologyentity already shaped right, frontend hasoncology-emr. Add gate rules + screening form slots. - HD: 60% reuse —
HemodialysisSessionDatafrontend types exist, but backend schema needs first-class lift. Build it as atreatment-series-engineinstance 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:
- Phase 1 of each domain migration = add adapter that calls the engine alongside existing code
- Phase 2 = the new screening + MD review flow becomes the recommended path, old UI stays for backward-compat
- Phase 3 (optional) = retire old UI once new flow is proven
No big-bang. No flag day.
Open questions
-
One plan entity or one per domain?
- Option A: extend the generic
TreatmentPlanand 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
- Option A: extend the generic
-
Where does
gateRuleslive for new domains?- On the plan entity itself (mirrored from RT)
- Or on a separate
screeningGatePolicyentity referenced by plan - Recommendation: on the plan — colocation makes the policy immutable per plan and easy to inspect
-
Should this share infra with
policy_gates?policy_gatesis 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.
-
Trend visualization shared component?
- MD review dialog shows sparklines across sessions
- Build once in
treatment-series-engine/mdReview/TrendSparkline.tsx, takesmetric: string+sessions: Session[] - All domains get free trend visualization
-
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
parentPlanTypecolor/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. |