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, feed, 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), a feed track (feed), or a class type’s calendar (schedule). Apply expands cells to dates and materializes into the appropriate target table — workout-assignments, workout_feed_posts, 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-only fields bound at creation:
targetClassTypeId,daysOfWeek[],startTimes[], optionaldefaultLocationId,defaultCoachMembershipId. - 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, recurrence (daysOfWeek+startTimes) is derived from the existing class sessions.- Three apply converters (
applyToCoaching,applyToFeed,applyToSchedule) with mode-specific conflict handling and target-validation rules.
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 —
applyToSchedulewrites bothclass_sessions(recurrence × duration) anddaily_programming(one row per workout cell that lands on an active day). - Workout feed posts (
workout_feed_posts) —applyToFeedwrites one row per workout cell withpublishAt = local-date @ publishTimeOfDayin the org’s timezone.
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. Thedaily_programmingrows are alsodraft. TODO: verify whether a single publish action covers both, or two separate publishes are needed.from-historyfor schedule mode derives recurrence in UTC weekday/time — does not currently translate throughorg.timezone. TODO: verify and add ticket if confirmed.