Canonical source: docs/claude/programs-protocols.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.
Programs & Protocols + Biomarker Tracking
Programs (also called Protocols) are structured care plans with scheduled activities, exercises, and supplements.
Architecture Overview
Basis Flow Web (Staff) ---assign_protocol---> clinic_service (Firebase Function)
Basis Hybrid (Client) ---event completion---> sync_user_event_to_clinic (Trigger)
Basis Web (Client Portal) ---read-only--->
FIRESTORE:
clinicsv2/{clinicId}/clinic_users/{uid}/
protocols/{protocolId} <-- SINGLE SOURCE OF TRUTH
events/{eventId} <-- Staff reads/writes here
users/{uid}/
protocols/{protocolId} <-- Mirror (backup)
events/{eventId} <-- Client writes here
recurringEvents[] <-- Habit definitions
Current mechanisms (updated 2026-07-18 — epic #398)
The sections below the intro predate these; this block is the current source of truth for how protocols behave now. All in functions_clinic.py unless noted.
- Canonical habit shape (#387): every write path —
assign_protocol,update_protocol, the richrecurringEvents, and BOTH protocol mirrors — serializes habits through the ONE functionserialize_protocol_habit()(PROTOCOL_HABIT_SCHEMA_VERSION). Emits id/type(resolved)/name/rule/ui/description/notes/defaultSupplements/normalized workoutPreset/presets/meal fields, so the shape can't fork (the old minimal mirror used to drop defaultSupplements/description/notes → supplements invisible on web). Assign writes both mirrors in one atomic batch; proto docs carryversion+changeLog[]audit. - #8 notes hierarchy: three levels persist to both mirrors → client-visible: protocol
description→ activitynotes/description→ item-levelnote(per-exercise onworkoutPreset.exercises[].note; per-supplement ondefaultSupplements[].note/supplementPreset.supplements[].note). - Recurrence data-loss stopper (#397):
protocol_reconcile.event_has_preservable_state(data)—update_protocolPRESERVES occurrences carrying completion/note/move/override state instead of the old delete-all-and-regenerate (which wiped completions on any edit). Full per-instanceexceptions[]+ edit-scopes (this / this-and-future / all) are still TODO. - Lifecycle + auto-expiry: status
active | ended | stopped | deleted. Two ways to end, both run the SAME ended-handler inupdate_protocol(deletes futureexpected-*events + generates the frozen efficacy summary): (1) manual — the "End Protocol" button in basisflow-webPlanTab.tsx(request_type:'update_protocol', status:'ended'); (2) auto — theexpire_protocolsdaily scheduler (08:00 UTC) finds active protocols pastscheduledUntiland ends them. It runs BOTH a string-bound and datetime-bound collection-group query (Firestore filters are type-segregated → catchesscheduledUntilstored as a date string AND as a legacy Timestamp). Assign defaults to end-dated now (AssignProtocolModal: pre-fills from template duration, else a 12-week default; "Ongoing" is an explicit opt-out) — because efficacy only renders for ended protocols. - Efficacy (#399, #7/#12):
_generate_protocol_summarycomputes adherence +biomarkerProgress[{code,name,startValue,endValue,target,direction,source}], FREEZES the window atscheduledUntil(notnow), and writes to BOTH mirrors — the user mirror is client-readable, so the app/portal render it and Atlas (clinical_agent_stream._exec_get_protocols) narrates it. Date-only endpoints are coerced to UTC-aware before comparison. - Lab-type biomarker routing (#6): biomarkers whose code is in the lab registry read start/end from
users/{uid}/labs(analyteKey), nothealthSummaries(which only holds wearable/nutrition series); wearable codes still map via_BIOMARKER_TYPE_KEY(#505). EachbiomarkerProgressentry carriessource: 'labs'|'healthSummaries'. - Activity↔booking (#10): an activity may carry
linkedServiceId(+bookingRequired).bridge_booking_to_protocol()auto-completes the day's occurrence (completionSource:'booking') when the client attends the linked session — wired into BOTH group (update_group_attendee_statusconfirmed) and 1:1 (update_appointment_statusCONFIRM) attendance; idempotent. The clinician mapping UI (setlinkedServiceId) is #543, still TODO in basisflow-web. - Migration:
tools/migrate_protocol_shape.pybackfills existing (active-only) protocols to the canonical shape. Verify tools:tools/verify_protocol_reconcile.py,tools/verify_booking_bridge_and_lab_progress.py(emulator gates).
Single Source of Truth
clinicsv2/{clinicId}/clinic_users/{uid}/protocols/{protocolId} is the authoritative source for:
- Protocol metadata (title, description, status, dates)
- Habit definitions with
workoutPresetandsupplementPreset - Activity scheduling rules
All platforms read protocol details from this location. The users/{uid}/protocols path is a mirror for backup/fallback only.
Data Models
Protocol Document (protocols/{protocolId})
interface ProtocolDocument {
protocolId: string; // e.g., "huberman_fitness_protocol_1234567890"
clinicId: string; // REQUIRED - enables client apps to find clinic path
title: string;
description?: string;
status: 'active' | 'completed' | 'cancelled' | 'paused' | 'ended';
assignedAt: Timestamp;
scheduledFrom?: string; // ISO date
scheduledUntil?: string; // ISO date
habits: HabitDefinition[]; // Array of activities with presets
biomarkers?: any[];
clinicalEvidence?: ClinicalEvidenceItem[];
expectedOutcomes?: ExpectedOutcome[];
}
interface HabitDefinition {
id: string; // Matches event.habitId
type: string; // BasisEventType (e.g., "habitExercise", "habitSupplement")
name: string;
rule: RecurrenceRule;
ui?: { at: string; by: string[] };
workoutPreset?: WorkoutPreset;
supplementPreset?: SupplementPreset;
}
interface WorkoutPreset {
exercises: Exercise[];
}
interface SupplementPreset {
id?: string;
name: string;
form?: string; // "capsule", "tablet", "powder", etc.
dose?: { amount: number; unit: string };
}
Event Document (events/{eventId})
interface EventDocument {
id: string; // Format: "expected-{habitId}-{timestamp}"
protocolId: string; // Links to protocol document
habitId: string; // Links to specific habit in protocol.habits[]
clinicId: string; // Enables sync trigger to find clinic path
eventType: string; // BasisEventType
name: string;
start: Timestamp;
end: Timestamp;
status: 'upcoming' | 'completed' | 'missed' | 'skipped';
source: 'expected'; // Indicates generated from protocol
exercises?: ExerciseHistory[];
supplements?: SupplementV1[];
note?: string;
completionStatus?: string;
}
Dart Models (Basis Hybrid)
// BasisEventV1 - hybrid/basiscore/lib/src/models/event/event.dart
class BasisEventV1 {
String? protocolId; // Links to protocol document
String? habitId; // Links to habit in protocol.habits[]
String? clinicId; // Enables finding clinic path for preset lookup
}
// BasisRecurringEventV1
class BasisRecurringEventV1 {
String? protocolId;
BasisWorkoutPresetV1? workoutPreset;
List<BasisSupplementV1> defaultSupplements;
}
Write Paths
1. Protocol Assignment (Staff -> Backend)
File: basis-functions/functions/src/functions_clinic.py -> assign_protocol()
Basis Flow Web (AssignProtocolModal.tsx)
| clinic_service({ request_type: 'assign_protocol', ... })
|
assign_protocol():
|-- 1. Write to user.recurringEvents[] (habit definitions)
|-- 2. Write protocol doc to clinicsv2/.../protocols/{id} (includes clinicId)
|-- 3. Write protocol doc to users/{uid}/protocols/{id} (mirror)
|-- 4. Generate expected events (next 90 days)
Write to clinicsv2/.../events/{id} AND users/{uid}/events/{id}
2. Protocol Update (Staff -> Backend)
File: functions_clinic.py -> update_protocol()
- Updates habit definitions, status, scheduling
- Regenerates future expected events
- Maintains dual-write pattern
3. Event Completion (Client -> Firestore -> Trigger)
File: functions_event_mirror.py -> sync_user_event_to_clinic()
Basis Hybrid (client completes activity)
| Writes to: users/{uid}/events/{eventId}
|
Firestore Trigger: sync_user_event_to_clinic
| Reads clinicId from event (or user doc)
|-- Mirrors to: clinicsv2/{clinicId}/clinic_users/{uid}/events/{eventId}
Read Paths
Basis Flow Web (Staff)
collection(db, 'clinicsv2', clinicId, 'clinic_users', clientId, 'protocols')
collection(db, 'clinicsv2', clinicId, 'clinic_users', clientId, 'events')
Basis Hybrid (Client)
// Fetches from BOTH paths, deduplicates
firestore.collection('clinicsv2').doc(clinicId)
.collection('clinic_users').doc(uid).collection('protocols')
firestore.collection('users').doc(uid).collection('protocols')
Basis Web (Client Portal)
collection(firebase.db, 'clinicsv2', clinic.clinicId, 'clinic_users', user.uid, 'protocols')
Key Backend Functions
| Function | File | Purpose |
|---|---|---|
assign_protocol | functions_clinic.py | Create protocol + generate events |
update_protocol | functions_clinic.py | Modify protocol + regenerate events |
backfill_protocol_events | functions_clinic.py | Repair missing recurring events |
sync_user_event_to_clinic | functions_event_mirror.py | Mirror client completions to clinic |
Key Frontend Components
| Component | Platform | File | Purpose |
|---|---|---|---|
AssignProtocolModal | Basis Flow Web | components/protocol/AssignProtocolModal.tsx | Create/assign protocols |
PlanTab | Basis Flow Web | app/(main)/clients/[id]/components/PlanTab.tsx | Display calendar + protocols |
RouteHabits | Basis Hybrid | view/routes/habits/route_habits.dart | List protocols + activities |
RouteSummary | Basis Hybrid | view/routes/summary/route_summary.dart | Event detail + preset display |
ProtocolsPage | Basis Web | app/portal/protocols/page.tsx | Read-only protocol list |
Activity Types
| Type | Description |
|---|---|
habitExercise | General exercise |
habitLowerBody / habitUpperBody | Strength training |
habitZone2 | Zone 2 cardio |
habitSupplement | Supplement intake |
habitMorningLightExposure | Light therapy |
habitCoffeeWait / habitCoffeeEnd | Caffeine timing |
habitNSDR | Non-Sleep Deep Rest |
habitHotColdTherapy | Sauna/cold plunge |
habitBreathwork | Breathing exercises |
habitMealEnd | Meal timing |
Curated Protocol Templates
File: hybrid/basisflow-web/lib/protocol-data.ts
Pre-built templates: Sleep Program, Fitness Program, Focus Program, Nutrition Program, Cardiac Rehab, Metabolic Health.
Activity Type Detection in Basis Flow Web
When protocol activities don't display in the ActivityDetailsDrawer, CHECK UI CONDITIONALS FIRST:
- Protocol activity types use
habit*prefix (e.g.,habitUpperBody,habitLowerBody) - The
isWorkoutLike()function inPlanTab.tsxmust recognize these patterns - Rendering conditions for presets must check for
hasAnyPresetnot justisWorkout && workoutData
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| Presets not showing in Basis Hybrid | Event missing habitId/protocolId | Check backend writes these fields |
| Completion not syncing to staff view | Missing clinicId on event | Ensure event has clinicId |
| Protocol not appearing in client app | Protocol doc missing clinicId | Reassign or backfill clinicId |
| Duplicate activities | Events written but not deduplicated | Check event ID generation |
Protocol Biomarker Tracking
Biomarker tracking allows clinicians to monitor health metrics throughout a protocol's duration.
Flow
- Template Creation (Programs Tab) — Select biomarkers from
BIOMARKER_CATALOG(lib/biomarker-catalog.ts), specify target direction - Protocol Assignment (AssignProtocolModal) — Auto-calculate 30-day baselines from healthSummaries, manual entry for weight/waist/BP/clinical scores
- Progress Display (PlanTab) —
BiomarkerProgressCardshows baseline -> current -> change, color-coded status - Copilot Integration — "Ask Copilot About This Protocol" builds
ProtocolCopilotContext
Key Files
| File | Purpose |
|---|---|
lib/biomarker-catalog.ts | Biomarker definitions grouped by category |
lib/biomarker-utils.ts | Baseline calculation, progress assessment |
lib/protocol-copilot-context.ts | Copilot context builder for protocols |
lib/protocol-data.ts | BiomarkerBinding interface |
components/protocol/BiomarkerSelector.tsx | Multi-select grouped by category |
components/protocol/BiomarkerProgressCard.tsx | Progress visualization |
BiomarkerBinding Interface
interface BiomarkerBinding {
code: string; // e.g., 'hrvs', 'rhr', 'labHbA1c'
displayName: string;
category: string; // cardiac, metabolic, labs, etc.
unit: string;
baselineValue?: number;
baselineDate?: string;
baselineSource?: 'auto_30d' | 'manual';
targetDirection?: 'increase' | 'decrease' | 'maintain';
targetValue?: number;
expectedOutcome?: {
deltaPctRange?: [number, number];
byWeek?: number;
};
}
Biomarker Categories
| Category | Examples | Data Source |
|---|---|---|
| cardiac | HRV, RHR, VO2 Max, Blood Pressure | Wearable, Manual |
| metabolic | Glucose (CGM), Fasting Glucose, HbA1c | CGM, Lab |
| body_composition | Weight, Waist, BMI, Body Fat % | Manual |
| activity | Steps, Active Calories, Zone Minutes | Wearable |
| sleep | Sleep Duration, Sleep Score | Wearable |
| labs | Lipid Panel, CBC, Thyroid, Vitamins | Lab |
| manual | Mood, Energy, Pain, PHQ-9, GAD-7 | Manual Entry |
Baseline Calculation
Automatic (30-day average): Query healthSummaries for last 30 days, match biomarker code to type/vt field, calculate mean.
Manual entry required for: Weight, waist circumference, blood pressure, clinical scores (PHQ-9, GAD-7, etc.)
Progress Assessment
Requires >=3 data points for valid assessment. Calculates % change from baseline and assesses against target direction.
Adherence Requirement
Biomarker analysis requires 80% protocol adherence to be meaningful. Display warning if adherence < 80%.