# Apple Watch Running Workouts

A static, no-build web tool for generating structured `.workout` files for the
[WorkOutDoors](https://apps.apple.com/app/workoutdoors/id1345458224) Apple Watch app.
AirDrop or share a generated file to your iPhone, open it in WorkOutDoors, and it
becomes a structured workout you can run on your Apple Watch — with vibration/pace
alerts through warmup, work intervals, recovery, and cooldown.

Two tools in one page:

- **Halve marathon schema** — the full 16-week "Halve marathon onder 1u45" plan
  from [running.nl](https://running.nl/trainingsschema-halve-marathon-onder-de-1-uur-45-minuten/)
  (published there as images, transcribed into `schedule.js`), converted session-by-session
  into downloadable `.workout` files, individually or as one zip.
- **Custom workout** — build any workout by hand: a warmup, one or more repeatable
  **blocks** of steps (distance, time, or open/manual targets), and a cooldown.
  Warmup, cooldown, and each step can carry one alert: pace, heart rate, or cadence.

## Why not push straight to the Watch?

Apple doesn't expose a web API to create Watch workouts directly. This tool
targets WorkOutDoors' file format instead, since it can import `.workout` files
shared from the Files app / AirDrop / Mail with no companion app of our own required.

## File format

`.workout` files are **protobuf-encoded**, with no header or magic bytes. The
layout below was reverse-engineered by diffing real exported files — there's no
official spec.

### Top-level message

| Field | Type | Meaning |
|-------|------|---------|
| 9 | string | Workout UUID (uppercase), must be unique per file |
| 11 | nested | Workout body |
| 1000 | varint | `1` (schema version?) |
| 1002 | varint | `5` (format version?) |

### Workout body (field 11)

| Field | Type | Meaning |
|-------|------|---------|
| 1 | varint | `37` constant |
| 2 | varint | `3` constant |
| 3 | string | Title shown in the app |
| 4 | nested | Warmup goal |
| 5 | nested, **repeated** | Main block(s): each a sequence of steps + a repeat count |
| 6 | nested | Trailing block: cooldown goal, or a plain activity marker |

### Goal block (time, distance, or open target)

`goal_type`: `1` = time, `3` = distance, `4` = **open** (manual / press-the-Digital-Crown-to-advance,
no measurable target — carries no `(unit, value)` pair at all, just the bare type). For
`1`/`3` it's a protobuf `oneof` — time goals carry their `(unit, value)` pair in field 2,
distance goals in field 4. Units: time → `1`=seconds, `2`=minutes; distance → `1`=meters,
`2`=km. Values are IEEE-754 doubles (`fixed64`).

### Alerts (pace, heart rate, cadence)

A goal container — the warmup (field 4), the cooldown (field 6), or a step's
own payload — has the shape `{ field 1: Goal, field 2: optional Alert }`. The
Alert is a small discriminated union, identified by a leading `(type, mode)`
varint pair, whose actual payload lives at field number **`type + 2`**:

| Alert | `type` | Payload field | Payload shape |
|-------|--------|----------------|---------------|
| Pace | `2` | 4 | single value: `{ field1: BoundBlock(speed) }` (`mode`=`1`) — or a range: `{ field2: { field1: BoundBlock(slow), field2: BoundBlock(fast) } }` (`mode`=`2`). Speed in **m/s** (`speed = 1000 / seconds_per_km`) |
| Cadence | `3` | 5 | `{ field1: { field1: spm (varint), field2: { field1: 2, field2: fixed64(1.0) } } }` |
| Heart rate | `5` | 7 | `{ field1: { field1: zone (varint) } }` |

A pace alert additionally carries a **subtype** in its leading `type` field's
sibling (field 1 of the alert wrapper, not to be confused with the `type`
discriminator itself): `1` = current pace, `2` = average pace. The classic
"target pace range for this interval rep" alert (used throughout the
half-marathon schedule) is subtype `2` (average) with a two-bound range;
a single pace value (no range) uses the same subtype field with a one-bound
payload instead.

**Correction:** an earlier version of this tool had heart rate and cadence
swapped — the `170`-valued alert (plausible as a literal spm figure) was
labelled "heart rate", and the small `2`/`3`-valued alert (plausible as a
zone index) was labelled "cadence". A real WorkOutDoors export with both
alert kinds clarified the correct mapping, which the table above reflects.

All shapes (pace range, pace single-value with current/average subtype,
cadence, heart rate) are confirmed byte-for-byte by reconstructing three real
WorkOutDoors exports — the last with a heart-rate zone alert on a work step
and two pace alerts (one current, one average) on separate open steps — and
diffing against the originals. All three reconstruct **exactly**, byte for
byte, once the UUID is pinned.

The heart-rate zone value and the exact meaning of the `1.0` constant paired
with the cadence value (mirroring the same constant in the pace `BoundBlock`)
remain unconfirmed beyond "plausible, and byte-exact to reproduce."

### Main block (field 5, repeated)

A list of `Step`s (`marker`: `1`=work, `2`=recovery — doesn't have to strictly
alternate, and both can be `1`), each with a goal + optional pace, followed by
a repeat count for that step sequence. Field 5 is a **repeated field at the
message level** — a real export can contain several of these back to back,
each its own independent phase with its own repeat count (confirmed against a
real multi-block WorkOutDoors export; see "Custom workout" in the UI, which
lets you add as many blocks as you like). One block is general enough to express:

- a single continuous run (one step, repeat count 1)
- classic `N x (work + recovery)` intervals (two steps, repeat count N —
  note recovery repeats after *every* rep, including the last one)
- irregular multi-segment sessions, by just listing every step once with
  repeat count 1 (e.g. `3K @pace1, 1K jog, 2K @pace2`)
- open/manual reps with no target at all (goal_type 4), e.g. "press to
  advance, twice"

Multiple *blocks* let you chain independent phases, e.g. a set of open reps
followed by a separate `N x (400m + 5min)` interval group — each phase keeps
its own repeat count rather than forcing everything into one flat sequence.

## Known limitations / approximations

- Confirmed byte-for-byte, by reconstructing real example files (two full
  multi-block WorkOutDoors exports) and diffing against the originals: single
  continuous runs, `N x (work+recovery)` intervals, irregular multi-segment
  sequences, meters as a distance unit, open/manual (goal_type 4) steps,
  multiple sequential main blocks (repeated field 5), and pace/heart-rate/cadence
  alerts on the warmup, cooldown, or any step. Anything beyond that is
  unverified — **always test a new file shape in WorkOutDoors before trusting it**.
- The exact real-world *meaning* of the heart-rate zone value and the cadence
  spm value is inferred, not confirmed from a spec — see "Alerts" above.
- The schedule includes two "duurloop met versnellingen" sessions (weeks 4
  and 8: a continuous run with short pace pickups spread through it). The
  file format has no primitive for "continuous run with periodic surges", so
  these are approximated as time-based `N x (pickup + steady jog)` intervals
  with an estimated recovery duration — close in practice, not an exact
  transcription.
- Week 12's Friday run ends with "3 short accelerations of 10-12 seconds" in
  the source plan; that detail isn't encoded in the generated file (shown as
  a note in the UI instead) since it doesn't fit cleanly alongside the run's
  main distance goal.
- The `1.0` constant inside the pace bound block, and the exact meaning of
  body fields `1`/`2` and top-level fields `1000`/`1002`, are unconfirmed —
  they're copied verbatim from the real example files.

## Files

- `workoutFormat.js` — the protobuf builder (pure, dependency-free).
- `zip.js` — minimal store-method ZIP writer for the "download all" bundle.
- `schedule.js` — the 16-week schedule data.
- `scheduleToWorkouts.js` — converts a schedule session into a `.workout` file.
- `app.js` / `index.html` / `styles.css` — the UI.
