The template never learns your name
A trainer builds a workout once and runs it for a room of people who are not the same. The data model that survived is three tables with a one-way street between them, and the model it replaced was the obvious one.
4 min readMicroGym · Workout stage
In the gym app a trainer builds a workout once: sets, rounds, the movements in each set with their reps and weights. Then the same workout is run by a room full of people who are not the same. One has a shoulder that rules out overhead work. One gave an intake answer that says she should do the regression of a lift. The template describes the intent. It cannot describe the outcome for each person, because there is a different outcome for each person.
The obvious way to handle that is to copy. Make a per-member version of the workout when the session starts, edit the copy, run the copy. It is obvious because it is what you would do on paper. It is also how the app did it for its first year and a half.
The copy that was deleted
Until 15 October 2026 there were two tables for exactly this: a per-member workout template and a per-member movement-settings row, created at session time, edited by the trainer, reset by a dedicated function. They worked. They were also a second place where the truth about a workout lived, and the second place is where the drift goes. Edit the template after the copy is made and the copy does not know. Fix a movement's defaults and every copy keeps the old ones. The tables were removed as dead code because by then nothing wrote to them, and the reason nothing wrote to them is the design below.
Plan, delta, outcome
What runs now is three tables with a strict direction between them. The template is the plan: workouts, sets, the movements in each set. Nothing in that chain has a member column or a session column, so a per-member version of a template is not something the schema can hold. The substitution is the delta: one row per member per movement, saying use this other movement instead, or use this variation of the same one. The performance is the outcome: what was recorded for that member in that session in that round, with the resolved movement and a pointer back to the original. The three are joined when the grid renders and never merged on disk.
Where the delta comes from
When a session is created the app reads the template, reads the gym's substitution rules, reads each member's most recent completed assessment, and writes a substitution row wherever a rule's answer matches a member's. A rule is narrow: one question, one answer value, one movement, one replacement. There are no injury flags and no strength tiers in the live path. Both existed, both were retired in September 2026 in favour of the single question-and-answer rule, and both are still in the tree as functions nothing calls. The row written at session creation is not scoped to the session. It is the member's default for that movement, and it carries forward. A trainer can write a one-session override by hand, and the override wins over the default when the grid renders.
Resolving at the grid
The tablet in the room shows the template's rows down the side and six members across, paging when there are more. For each cell it asks one question: does this member have a substitution for this movement, preferring a session override, falling back to a default. If so, the cell shows the replacement's name and takes the replacement's properties, timed or counted, per side or not. If the replacement is the same movement with a variation label, the cell says so rather than pretending a swap happened. The movement recorded against the performance is the one the member did, with the original kept as a back-pointer, so the history reads the way the room did.
What it costs
Three views instead of one, and the separation is held by structure, not by a check. No trigger or constraint forbids session code from writing to the template tables, and no policy does either. What forbids it is that the template has nowhere to put a member, and that every write to those tables lives in the workout builder. On 23 October 2026 that was true: fourteen write sites, three files, all in the builder. It stays true only as long as the next person reads this before adding a fifteenth.
The second cost is in the delta. Because the auto-created row is a member default rather than a session row, it is written once and not revisited. A newer assessment answer for the same member and movement does not replace it; the guard that stops duplicates also stops updates. That is a product decision wearing a bug's clothes, and it is written here so it gets decided rather than discovered.
Why it belongs on this page
The anatomy row for this stage said two views. The source says three, and it said the delta was per session when it is per member. Both corrections came from reading the schema, the service that writes it and the component that reads it inside the same hour, which is only possible when the same person owns the migration and the grid. The design is good. The description of it was less careful than the code, and the code is what gets to be right.