Program templates
What this is
A program template is a reusable, multi-week programming structure — a grid of (week, day, slot) → workout | rest | note cells, tagged with a deliveryMode (coaching or schedule). A coach designs the structure once (“Hyrox 8-week block, Mon/Wed/Fri at 7am”) and then applies it to a target: a set of athletes (coaching) or a class type’s calendar (schedule). Apply expands cells to dates and materializes into the appropriate target table — workout-assignments or class_sessions + daily-programming. FIT-74 is the originating issue.
Who uses it
| Persona | Role |
|---|---|
| Owner / Coach | Authors templates, runs preview, applies to athletes / programs / class types. |
| Member | Indirect — sees materialized assignments/sessions, never the template itself. |
| Spotter agent | TODO: verify whether agent currently authors templates or only triggers apply. |
Persona impact
- Coach stops re-creating the same 8-week block by hand for every new cohort. One author surface (the template builder), three apply targets.
- Owner gets one canonical source for “how we program Hyrox blocks” so the offering is consistent across coaches.
- Member sees consistently-published workouts/sessions; the template indirection is invisible.
Capabilities
- CRUD a template header (name, description,
deliveryMode,durationWeeks,tags[],isActive). - Schedule-mode recurrence bound at creation as
slots[]— one per class type, each with independent(day, time)occurrences, optionallocationId/coachMembershipId. - Replace the entire cell grid in one call (
POST /:id/workoutswithcells[]). POST /:id/duplicate— full clone, name suffixed(copy).POST /:id/preview— return what apply would produce, with no DB mutation.POST /:id/apply— materialize to the target mode in one transaction.POST /from-history— derive a template from already-materialized rows (save-as-template); for schedule sources, the slot’soccurrences[]are derived from the existing class sessions.- Two apply converters (
applyToCoaching,applyToSchedule), both honouringconflictMode(skip|overwrite|abort). Schedule conflicts are same-class-type time overlaps;overwritenever replaces a session that has bookings.
Relationship to other features
- workouts —
program_template_workouts.workoutIdreferences the library workout. Cells withkind='rest' | 'note'carry no workout pointer. - workout-assignments —
applyToCoachingis the bulk path that insertsworkout_assignmentsrows (one per cell × athlete). Lazy-fork pointer state on insert. - daily-programming — no longer written by apply. Since FIT-250
applyToScheduleis pure recurrence (class_sessionsonly); workouts attach per class-type-per-day through the daily board.
Current status
Shipped. Header CRUD, cells upsert, preview, apply across all three modes, duplicate, and from-history are all live (apps/api/src/program-templates/program-templates.service.ts, ~1119 lines). deliveryMode='course' is explicitly excluded from program_templates_delivery_mode_chk.
Known gaps / open Linear issues
- FIT-74 — Founding spec. Shipped.
applyToFeedskipskind='note'cells (“notes deferred to v2”).kind='rest'is silently skipped (no feed concept of rest day).applyToCoachingdoes not push notifications on bulk insert; per-athlete pushes happen via the regular assignment flow (e.g.assignPersonaldoes push;applyToCoachingdoes not). TODO: verify whether this is intentional.applyToCoachingconflict modes areskip(default),abort,overwrite. Overwrite soft-deletes the existing conflicting row; results referencing it survive via FK.applyToSchedulealways writes sessions indraftstatus — they must be published manually from the schedule.from-historyfor schedule mode derives recurrence in UTC weekday/time — does not currently translate throughorg.timezone. TODO: verify and add ticket if confirmed.