GymSync Block Format v2 (Specification)
Status: draft for implementation · 2026-07-10 · schema: gymsync-block-v2.schema.json
One JSON file = one training block (4–16 weeks). The file is both the plan
(written by a coach: human or AI) and the log (appended by the athlete's
app). It travels in both directions: load it into GymSync to train, hand it back
to the coach to review and to generate the next block.
Design principles
1. Plan ≠ log. sessions (with goals, nutrition, phases, exercises)
is the immutable intent; log is append-only fact. Apps MUST NOT rewrite the
plan to record results: that is what makes "planned vs performed" analysis
possible. Mid-block plan edits are recorded per affected logged session in
log.sessions[].amendments.
2. Stable ids, display names. Exercises are defined once in the exercises
catalog and referenced by exerciseId. PR tracking, muscle volume and
cross-block comparisons key on ids, never on names. work[].variant
("Heavy", "Volume", "drop set") is display-only.
3. Cardio is part of the plan. Runs, rides, swims and HIIT are sessions with
type + cardio targets: one calendar, one adherence metric, one load model.
4. Everything needed to coach is in the file: goals with baselines and test
sessions, nutrition targets (incl. training-day carb delta), phase structure.
5. Extensible, strictly. Unknown keys are rejected except x-* extension
fields, which tools SHOULD preserve on read-modify-write.
File conventions
- Name:
<meta.id>.gymsync.json; recommended location: an iCloud/Drive folder
shared between the app and the coach (GymSync uses …/Gym/blocks/).
- Encoding UTF-8; dates
YYYY-MM-DD; timestamps ISO-8601 with offset. meta.units(kg|lb) applies to every load and body-weight value in the file.
Semantics that tools must agree on
Volume (tonnage)
set tonnage = reps × load × armFactor, where armFactor = 2 when the
exercise has perArm or dualDB, else 1.
loadfor a logged set: itskg; if absent and the prescription/log uses
stages: style: "perSet" → the set's stage; style: "dropRounds" → the SUM
of all stages (one "set" = one full drop round).
bodyweight: true→ add the session'sbodyWeightKgsnapshot to the load
(kg then means added external load). Apps SHOULD capture bodyWeightKg
at session start so history never drifts as the athlete's weight changes.
Session matching & completion
A plan session is done when a log.sessions[] entry references it via
planSessionId and the entry is not skipped: true (see Skipped
sessions). Completion is scoped to this file (a new block may reuse ids)
like W1D1 freely. Unplanned workouts log with type and no planSessionId.
Estimated 1RM
Tools SHOULD use Epley (kg × (1 + reps/30)) when displaying e1RM trends so
numbers agree across apps and coaches.
RPE
0–10 scale in practice 5–10; 8 = two reps in reserve. Apps SHOULD prompt for
the main lift's RPE at session finish if not captured per set.
Wearable data
watch (strength) and cardioResult (cardio) carry the summary a wearable
recorded: duration, avg/max HR, kcal, and zoneMin (minutes in 5 HR zones)
(zone bounds are the athlete's own; publish them in x-hrZones on meta if
tools need to agree).
Structured intervals (additive, v2)
cardio.intervals describes treadmill/track interval structure as an ordered
list. Each item is either a named phase (phase + sec or meters) or a
repeated block (repeat + work/rest legs). Every phase/leg SHOULD carry
pace ("m:ss" min/km): with pace set, duration is derivable for distance
phases and distance for timed phases, a watch app can then run the whole
session hands-free (countdowns, belt-speed hints 3600/paceSec, honest
distance from the plan). The rest after the final rep is implicitly dropped.
```json
"cardio": { "kind": "interval", "hr": [150, 172], "intervals": [
{ "phase": "rozgrzewka", "sec": 600, "pace": "5:40" },
{ "repeat": 6, "work": { "sec": 180, "pace": "4:40" },
"rest": { "sec": 120, "pace": "7:00" } },
{ "phase": "cooldown", "sec": 300, "pace": "6:30" }
] }
```
Meal plan (nutrition v2, additive)
nutrition grew from flat targets into a full meal plan (July 2026); the old
trainingDayCarbsDelta is DEPRECATED (ignored when dayTypes present).
- Day-flex targets.
dayTypesmaps a session day type → template:
target(day) = weight × weeklyBudget / Σ(week weights) (days differ,)
the weekly mean holds the budget (carb-cycling emerges). Day type resolves
from the plan's sessions (strength wins; else the cardio kind; else rest)
with fallback families (tempo→interval, zone2/recovery/technique/run→easy,
deload→rest, test→strength).
- Meals.
locked= ritual anchors (never rescaled; may carryruleand
product.name/product.recipeId); flex = water-filled to the day target
within portion bounds. intra slots (during-session fueling) belong on
long-endurance days. Per-date exceptions live in dayOverrides.
- Recipes. Top-level
recipes[]is a MIRROR of the athlete's collection
(app re-injects on every save) and the delivery vehicle for coach proposals:
apps MERGE by id on import (upsert, never delete); coach owns tr-* ids.
Ingredients carry per-100 snapshots; liquids use unit: "ml" (label values
1:1), optional density (g/ml) for future conversions. tags hold TWO
orthogonal axes in one array: slot × session type; empty axis = universal;
match rule: slot(S)∈axis1 AND (axis2 empty OR canon(dayType)∈axis2).
- Food log.
nutritionLog(day → entries, modes planned/swapped/extra)
is the athlete's append-only FACT: same barrier as log: coach revisions
must never touch it, and apps must preserve the local copy when applying
a revision.
Lifecycle
```
coach (AI/human) ──plan──▶ file ──load──▶ app (train, log appends)
▲ │
└───────────── review / next block ◀─────┘
```
Mid-block revision (coach edit)
The file may also travel back mid-block: the athlete exports the current
block (log included), the coach revises FUTURE sessions (swap exercises,)
trim volume after a weak week, even adjust goals (and the athlete loads the)
same file again. Apps replace the plan wholesale and reconcile against local
history, so completed work survives as long as the revision:
- keeps
meta.idandmeta.label(identity of the block), - never modifies
log(append-only (the athlete's facts),) - preserves the ids of already-completed sessions (future ids are free).
Hand-written log entries may use a bare date for started/finished
("2026-08-10"): apps anchor them at noon local time.
Skipped sessions
A deliberately skipped session is logged as
{"planSessionId": "W2D1", "started": "<date>", "skipped": true}.
A session counts as completed only when a log entry references its id
AND is not skipped: adherence tools must not count skips as done.
For a coach, skipped entries are the signal to revise the remaining weeks.
Cardio-only blocks
A block with no strength work at all (pure running base, tri prep without
gym access) is valid: keep the exercises key with an empty array and use
cardioTime/cardioDistance/measurement goals instead of lift goals.
Closing a block (optional): the app MAY write log.dailiesSnapshot (daily
kcal/sleep/HRV/steps/TRIMP) as an archive before the file is retired to history.
Versioning
gymsyncBlock: 2 is required. Breaking changes bump the integer; additive
changes extend the schema without bumping. v1 (gymsyncPlan: 1) files convert
mechanically with convert_v1_to_v2.py.
v3 outlook: if OpenWeight (openweight.dev) gains real adoption, v3 will consider
converging with it: either log-level interop (gymsync:* extension fields) or
upstreaming the "program as contract" concepts. Until then GymSync evolves independently.
Tooling in this directory
gymsync-block-v2.schema.json: JSON Schema (draft 2020-12), the source of truth.validate.py: schema + referential validation (ids, perSet lengths, weeks).convert_v1_to_v2.py: v1 plan → v2 block (catalog extraction, load objects,
phase inference).
examples/:hybrid-sample(strength + cardio + logged work),
run-base-sample (cardio-only: empty catalog, structured intervals),
block3-100kg-bench-2026 (a real production block).
PROMPT.md: the canonical interview→block prompt for Claude/ChatGPT/trainers.