Plan Scheduling¶
Overview¶
A patient's plan is a set of sessions on specific dates plus a schedule: which days sessions fall on, and what happens when the scheduled sessions run out or get missed. The schedule is read off the days the clinician lays out; there is nothing to set.
The promise to clinicians: what you prescribe is what continues. A plan built on Monday, Wednesday and Friday keeps going on Mondays, Wednesdays and Fridays; a missed session moves to the next scheduled day instead of piling onto the day after; nothing that a clinician removed ever comes back.
What the clinician sees¶
Nothing extra. The clinician lays sessions out on days in any plan editor (web classic modals, web HEP Builder, mobile Manage HEP), and the server reads the schedule off those days (see Reading the schedule off a plan): a Mon/Wed/Fri layout continues on Mondays, Wednesdays and Fridays; an unbroken run continues every day. Every editor works on one week and is pre-filled with what the plan would do anyway, so the week on screen is what the patient gets.
Past sessions also offer Repeat, which puts one back on a day of the coming week (see Repeating a past session).
There is no schedule control and no "stop after these sessions" switch in the apps: a control was built and removed the same day, because therapists already create a plan expecting it to repeat on those days. The engine keeps autoContinue (always on for plans made in the apps) and the schedule endpoints below for tools and older data.
How a plan behaves¶
Sessions land on the schedule¶
When a plan needs more sessions, the next session days are computed from the schedule — never by the AI and never on "the next day" by default. Each new day is a copy of the session the plan already has for that slot:
- Weekly schedules repeat the most recent session on the same weekday ("this Wednesday is last Wednesday"). A weekday the plan has never used repeats the most recent session day.
- Every-day and interval schedules rotate through the plan's last N session days, where N is the length of the content rotation read off the clinician's plan (a five-day AI plan rotates through all five days).
A copy carries the planned fields only: exercises, sets, reps or duration, weight, block, target pain and exertion, and the clinician's pre-exercise note. Actuals, patient notes, recordings and timestamps are never copied. Every copy records which session it repeats (continuedFromWorkoutId) and how it got there (source).
Missed sessions move to the next scheduled day¶
Dates are settled whenever anyone looks at the plan (the patient's dashboard, the clinician's plan views). Sessions whose date has passed keep their order and move to the plan's next session days; a later session moves only when an earlier one takes its day. Sessions that are neither late nor displaced stay exactly where the clinician put them, including a session on a day outside the schedule such as a clinic visit. ("Every other day" also keeps its rest day: a session scheduled inside the recovery gap after the last session actually done moves out of it.)
| Plan | Missed | What the patient sees |
|---|---|---|
| Every day: Mon A, Tue B, Wed C | Mon | Tue A, Wed B, Thu C |
| Mon/Wed/Fri: Mon A, Wed B, Fri C | Mon | Wed A, Fri B, next Mon C |
| Every other day, last done Tue | Thu | Sat (two days after the last session done) |
The original date stays on the session, so the patient's card can say "Rescheduled — was Mon" and adherence still counts the session against the day it was assigned. Patients can start any upcoming session at any time; dates are labels, not locks.
When the plan runs out¶
When fewer than three session days are left, four more are added on the schedule. It happens when the patient finishes a session, and as a safety net whenever the patient opens their plan and it is running low.
A patient who has no stored schedule and has not completed a session in 30 days is dormant: opening their plan, or opening an editor on it, adds nothing (it would only repeat old content). Their next completed session, or a clinician's save, starts continuation again.
Whether the copies are exact or AI-adapted depends on the existing two-level control: the tenant flag autoWorkoutGeneration and every connected therapist's AI Workouts toggle (both default to on). With AI on, the model is asked to adapt each copy to how the patient has been doing — progress or ease the dose, swap at most one exercise per session for a closely related one from the library — and its answer is validated against the base session and the library. Any failure, in the network or in the rules, leaves the plain copy in place, so the plan never runs dry because of the AI. With the tenant flag off, exact copies are added as long as the tenant flag planContinuation (also on by default) stays on. The clinician gets the same email as before ("Workout Plan Extended" / "Workouts auto-generated").
A clinic can switch continuation off altogether. With autoWorkoutGeneration and planContinuation both off (the admin Feature Flags page shows them as a pair of which at most one is on), nothing is added when a plan runs out: the patient does the sessions the clinician scheduled and then has none until the clinician adds more. A new personalized video goes only into the sessions that are left. Missed sessions still move to the next scheduled day, and the editors open on the sessions that exist instead of pre-filling the week. See Feature Flags.
Switching off removes nothing by itself: sessions the plan had already added stay. The clinician's next save of the plan drops the ones after the saved week, as every save does (see below), and in such a clinic nothing replaces them, so the plan then ends with the week the clinician saved. A run that was already adapting copies with the AI when the clinic switched off adds nothing either: the policy is checked again before anything is written.
The editor shows the real future¶
Every plan editor works on the same window: one week from today (Day 1 = today). Opening a plan for editing first settles missed sessions and fills that week with the sessions the plan would add anyway. Those pre-filled sessions are real and stay on the patient's plan if the editor is closed without saving (they are what the plan would have added by itself). For a clinic that switched continuation off, nothing is pre-filled: the week shows the sessions that exist. The clinician edits what the patient will actually get, and what they save is what repeats. Sessions the system had scheduled after the saved week are dropped on save and regenerated from the new plan when it next runs low (in a clinic that switched continuation off, they are not), so an edit can never leave stale copies waiting further out.
Nothing removed comes back¶
Each save records the plan's start (the first day of the saved window). Continuation only copies sessions from that day on. A clinician who removes an exercise from the plan never sees it reappear from an older session.
Repeating a past session¶
A therapist can put a session the patient has already had back on the plan with Repeat: in the web patient popup's Workouts tab (done sessions under Past, and a missed one while it is still dated in the past), on each row of the app's patient sessions list (done sessions) and on the app's session screen (done or missed). They pick one or more days of the same one-week window the editors use (today and the six days after), and the session's prescription replaces what each of those days holds.
A missed session rarely stays in the past: the first look at the plan moves it to the next session day (see above), so it is back on the plan already. Repeat settles missed sessions first, like the editors, and a session that has just moved to the coming days is refused with "This session has moved to the coming days. Change it in the plan."
- What is copied is what a continuation copy carries (the same code): exercises, sets, reps or duration, weight, per-set targets, blocks, target pain and exertion, the pre-exercise notes, and the session's name, description, session note and block notes. The patient's results, notes, recordings and times are never copied.
- Exercises no longer in the library (switched off since) are left out, and the therapist reads how many ("2 exercises are no longer available and weren't copied"). An exercise folded into the shared platform library is copied as the shared exercise, unless that one was switched off too. When nothing is left, nothing is copied.
- Replacing a day removes the sessions on it that are not done; done sessions stay. Days that hold exercises are marked before copying, with the warning that they will be replaced; there is no other confirmation. As in the editors, a session the patient opened more than an hour ago and never finished counts as not started and is replaced.
- Started days are locked. A day with a session the patient started within the last hour (the rule Manage HEP and the editors use) cannot be picked, and the server checks it again under the patient's plan lock. A day found started there is skipped and named in the confirmation ("Not copied to Fri, Oct 30: the patient has already started."). If the patient starts a day in the instant it is being replaced, nothing at all is copied and the therapist is asked to pick another day. The server never deletes a session started within the last hour, even one started a moment before.
- Days already over for the patient (a therapist a time zone behind the patient, late in the evening) are shown as unavailable and never written.
- The plan's schedule is left alone. The copy is the therapist's own session (
source: CLINICIAN, dated at midnight UTC,continuedFromWorkoutIdpointing at the session it repeats), but Repeat is a one-off: it does not change the stored schedule, the plan's start or the sessions the plan has already added after the week. A copy on a day off the schedule stays where it is, like a clinic visit, and continuation goes on as before (the copy becomes part of what it can repeat). - A patient without upcoming sessions keeps the button (team decision, 2026-10-26): Repeat simply puts the session on the chosen day.
The confirmation says where the session went ("Copied to Thu, Oct 30") with Open plan: on the web the plan editor; in the app Manage HEP, where the clinic has it (otherwise Done). Repeat itself has no flag and behaves the same in every clinic, OTERA and OKTA. Copying to another patient is what templates are for.
Reading the schedule off a plan¶
The apps send no schedule with a save: the server reads it off the days that have sessions, or keeps a rule set explicitly through the API earlier (schedule: { kind: "AUTO" } on a save re-reads it even over such a rule):
| Days with sessions | Schedule |
|---|---|
| An unbroken run of days | Every day (exactly Monday–Friday reads as Weekdays) |
| Gaps that line up week over week (Mon/Wed/Fri, Tue/Thu, …) | Those weekdays |
| Evenly spaced days that would crowd together if repeated weekly (days 1, 3, 5, 7) | Every N days |
| Fewer than two days | Unknown — the saved schedule (or every day) applies |
An inferred schedule is re-read on every clinician save unless the saved days still fit it (a partial window must not narrow it). A schedule the clinician chose stands until they change it, however older clients save.
Patients from before schedules existed get theirs read off the clinician's most recent sessions (using each session's original date, since the old logic packed missed sessions together) the first time their plan is touched.
Data model¶
model PatientPlanSchedule {
patientId Int @unique
kind PlanCadenceKind // WEEKLY | INTERVAL
weekdays Int[] // WEEKLY: 0 (Sun) .. 6 (Sat)
intervalDays Int? // INTERVAL: every N days (1 = every day)
cycleDays Int? // INTERVAL: session days in one content rotation
autoContinue Boolean // false => nothing is added when the plan runs out
origin PlanScheduleOrigin // EXPLICIT (clinician chose) | INFERRED (read off the sessions)
planStartDate DateTime? // continuation never copies sessions before this day
updatedById Int?
}
model Workout {
// ...
source WorkoutSource? // CLINICIAN, CLINICIAN_SINGLE, REPEAT_CONTINUATION,
// AI_CONTINUATION, VIDEO_CONTINUATION, APP_ENTRANCE, SELF_SERVE
continuedFromWorkoutId Int? // the session a copy repeats
}
Migration: 20260930003229_plan_schedule (additive; existing rows have source = null).
API¶
| Method | Path | Purpose |
|---|---|---|
| GET | /v2/doctor/patients/:patientId/plan-schedule |
The schedule, the continuation mode (AI / REPEAT, or OFF when the plan's autoContinue is off or the tenant switched continuation off) and a preview of the next session days (SCHEDULED ones and PROJECTED ones with the date each repeats) |
| PUT | /v2/doctor/patients/:patientId/plan-schedule |
{ schedule?, autoContinue?, realign?, mode? }. An explicit schedule is stored as the clinician's rule (its content rotation is read off the upcoming sessions); with realign on, the upcoming, unstarted sessions are laid out again on it from today (order kept, in-progress sessions stay), and realign: false (what the mobile sheet sends) leaves them where they are. mode: "auto" reads the schedule off the upcoming sessions without moving them. Changing only autoContinue keeps the schedule's origin and start |
| GET | /v2/doctor/patients/:patientId/repeat-session/days?today=YYYY-MM-DD |
The week Repeat copies onto, from the therapist's own today (accepted within a day of the server's UTC date): per day sessionCount and exerciseCount that a copy would replace, locked when the patient has started it and past when it is already over on the patient's calendar. Settles missed sessions first, like every plan read |
| POST | /v2/doctor/patients/:patientId/repeat-session |
{ workoutId, today, dates } (1 to 7 different days of that week). 200 { copiedDates, lockedDates, copiedExercises, skippedExercises, workoutIds } (lockedDates: days skipped as started or already over for the patient); 404 NOT_FOUND for a session that is not this patient's (or is a template); 409 NOT_PAST for an unfinished session on today or later; 409 DAY_STARTED with lockedDates when every chosen day was started, or when the patient started one while it was being replaced (nothing is written); 422 NO_AVAILABLE_EXERCISES when none of the exercises is in the library any more |
| POST | /v2/doctor/patients/:patientId/plan-schedule/prepare-editing |
Settles missed sessions and fills the editing window (windowDays, default 7, max 14) with plain copies; returns how many were created and moved, and in skipped why nothing was added, e.g. TENANT_CONTINUATION_OFF for a tenant that switched continuation off (AUTO_CONTINUE_OFF comes first for a plan stopped on its own) |
The plan saves accept the control's value alongside the sessions:
POST /v2/doctor/create-initial-plan,PUT /v2/doctor/edit-planandPUT /v2/doctor/patients/:patientId/hep-managementtake optionalschedule({ kind: "DAILY" } | { kind: "WEEKLY", weekdays } | { kind: "INTERVAL", intervalDays }, or{ kind: "AUTO" }to read the schedule off the saved days again even over a rule chosen earlier) andautoContinue.GET /v2/doctor/patients/:patientId/hep-managementpre-fills the week, returns one slot per day (sessions or placeholders) for today .. today + 6, and addsplanScheduleplussourceper session.
Schedule shape in responses:
{
"kind": "WEEKLY",
"weekdays": [1, 3, 5],
"intervalDays": null,
"cycleDays": null,
"autoContinue": true,
"origin": "EXPLICIT",
"planStartDate": "2026-09-28",
"updatedAt": "2026-09-29T15:04:05.000Z",
"updatedById": 282
}
Where the logic lives¶
| File | Role |
|---|---|
OktaPT-API/src/services/planSchedule/cadence.ts |
Pure calendar rules on integer epoch days: next session day, roll-forward, new session days, which session a new day repeats, inference |
OktaPT-API/src/services/planSchedule/schedule.ts |
Stored schedule; inference for patients from before schedules existed |
OktaPT-API/src/services/planSchedule/rollForward.ts |
Missed sessions → next session days, inside the patient's plan lock |
OktaPT-API/src/services/planSchedule/continuation.ts |
extendPlan: top-up after a completion or patient view, window fill for editors; tenantContinuationOf: the tenant's policy (AI / REPEAT / OFF) from its two flags |
OktaPT-API/src/services/planSchedule/aiAdaptation.ts |
The constrained AI adaptation and its validation |
OktaPT-API/src/services/planSchedule/repeatSession.ts |
Repeat on a past session: the week's days and the copy, under the plan lock |
OktaPT-API/src/services/planSchedule/capture.ts |
What a clinician's save means for the schedule; standalone schedule changes |
OktaPT-API/src/services/workoutReschedulingService.ts |
rescheduleOverdueWorkouts — kept as the entry point every read calls |
OktaPT-API/src/jobs/workoutCreationJobs.ts |
The completion trigger and the clinician email |
OktaPT-API/src/routes/doctor/planSchedule.controllers.ts |
The three schedule endpoints and the two Repeat endpoints |
OktaPT-FE/lib/api/planScheduleApi.ts |
prepareEditing, the only schedule call the web makes (the editors pre-fill the week with it) |
All writes that change a patient's dates — roll-forward, continuation, the personalized-video injection, the mobile HEP save and Repeat — take the same per-patient advisory lock, and continuation re-checks the plan after the AI call so two triggers never double-book a day.
Related¶
- Patient Workout Management
- HEP Builder (Beta)
- Mobile Doctor HEP Management
- Personalized Exercise Videos — the injection follows the schedule too
- Detailed changes (2026-09-29)