{"openapi":"3.1.0","info":{"title":"Gym API","version":"1.0.0","summary":"Plan workouts and read training history for a personal gym tracker.","description":"A personal, mobile-first gym tracker for one person. A coding agent writes the plan (planned workouts) through this API; the owner starts the next workout in the app and logs sets. Guides: [HTML reference with recipes](https://gym.abdellatif.io/api/docs) and [llms.txt](https://gym.abdellatif.io/llms.txt).\n\n## Status lifecycle\n\n- `planned`: a plan. This is what the agent writes.\n- `in_progress`: started in the app (`POST /api/workouts/{id}/start`).\n- `completed`: history. Only completed workouts count for history, records, suggestions and stats.\n- `skipped`: not done. Restore with `PATCH {\"status\": \"planned\"}`.\n\nTransitions go through the actions: **start** (planned or skipped → in_progress), **complete** (→ completed) and **skip** (planned → skipped; a completed or in-progress workout needs `force=1`). Setting `status` with PUT or PATCH keeps the timestamps consistent but does not create or clean up sets. Don't change or delete completed workouts unless he asks.\n\n## The next workout\n\n`GET /api/next` returns the `in_progress` workout (latest started), else the first `planned` workout ordered by `date` (unscheduled last), then `position` (lower first), then creation time. Past dates stay eligible: an overdue plan remains next (the app marks it overdue) until it is started, skipped, rescheduled or deleted. `upcoming` lists the other planned workouts in the same order. Give every plan a `date`; to put a plan first among those on the same day, give it a lower `position`.\n\n## Referencing exercises\n\nItems point at an exercise with `exercise_id`, or with `exercise` (a name). A name is matched in this order: an exercise's name, then an alias (both trimmed, case-insensitive), then loosely (case, spaces, hyphens, apostrophes and a plural s don't matter: `Strict Pull Up`, `strict pull-ups` → Strict Pull-Up; `Captains Chair Knee Tuck` → Captain's Chair Knee Tucks), then a name whose slug is an exercise id. An exercise's own name always beats another's alias. When nothing matches, a new exercise is created: its id is the slug of the name (`Cable Crossover` → `cable-crossover`), muscle group, equipment, tracking (`reps` for bodyweight moves, `time` for holds and planks) and per-side are guessed from whole words in the name, and the write response lists it in `created_exercises`. A new exercise has no history, so the app has nothing to prefill. Look names up first with `GET /api/exercises?q=…` (every word must appear: `q=pull up` finds Strict Pull-Up), and check `created_exercises` after writing: a different movement name (`Leg Extension` vs `Leg Extension Machine`) still splits the history. Fix a duplicate with `POST /api/exercises/{id}/merge`.\n\nThe item's `name` defaults to the exercise's canonical name. Send `name` as well to show a variant (\"Paused Bench Press\") and keep the history link. A new item with only `name` that is exactly an exercise's name or alias links to that exercise. Any other item with `name` and no exercise is a free-form block (AMRAP, circuit, cardio): no sets, no suggestion, logged as text in `result_text` (send `exercise_id: null` to force a free-form block).\n\n## Prescriptions\n\nPut the plan in `prescription` the way a coach writes it; he sees this text as is. The server parses it into targets (`target_sets`, `target_reps`, `rep_min`, `rep_max`, `target_seconds`, `rest_seconds`, `rir`) and a `cue` from the extra lines. Any of those fields sent in the same request overrides the parsed value (null included); leave them out unless the parser gets it wrong. A new `prescription` is parsed again on update, and target fields that just repeat the stored values are ignored then (an edited GET response); clearing it (null or `\"\"`) clears its targets and cue. Line breaks are `\\n` inside the JSON string. Preview with `POST /api/parse`.\n\n| Prescription | Parsed as |\n|---|---|\n| `3 x 12-10-8 reps, rest 30-60s\\n*Go down in 3 sec*` | 3 sets of 12, 10, 8 (range 8-12), rest 45 s, cue \"Go down in 3 sec\" |\n| `3 x 10-12 reps, rest 1-2 min` | 3 sets, range 10-12, rest 90 s |\n| `3x 30-40s, rest as needed` | 3 sets, hold 40 s, rest null |\n| `3 x Max Reps (-1), rest 1-2 min` | 3 max-rep sets (`target_reps` [null, null, null]), rir 1, rest 90 s |\n| `3 x 5 reps @2 RIR, rest 2 min` | 3 sets of 5, rir 2, rest 120 s |\n| `3 x 8-10 reps/side, rest 30-60s` | 3 sets, range 8-10, rest 45 s, per side |\n| `4 sets, 10-8-6` | 4 sets of 10, 8, 6, 6 |\n| `3 x 12, 10, 8 reps` · `3 x 12/10/8` · `3 x 12 10 8` | 3 sets of 12, 10, 8 (commas, slashes and spaces work like dashes) |\n| `3 x 10-12 reps, tempo 3-1-1-0` | range 10-12, cue \"Tempo 3-1-1-0\" (a tempo is never a rep scheme) |\n| `3 x 10-12 reps @2 sec pause at the top` | range 10-12, cue \"2 sec pause at the top\" |\n| AMRAP, EMOM or a list of circuit moves | free-form: no set or rep targets (rest only) |\n\n## Supersets and labels\n\n`label` groups items. Consecutive items that share a letter are one group done in rotation: `A1` + `A2` is a superset, `A1`-`A3` a tri-set, four or more a circuit. A bare letter (`D`) is a single block. Items appear in `position` order (the array order you send), so keep each group together and the letters in order. His recent programs are three tri-sets per day with a core exercise in the third slot (A3, B3, C3).\n\n## Writes and merge rules\n\n- `POST /api/workouts` creates a workout with nested items (and sets) in one atomic call. 409 if the `id` or `external_id` already exists, so a retried POST never duplicates.\n- `PUT /api/workouts/{id}` creates or updates the workout with that id (201 or 200) and merges:\n  - Workout fields you omit keep their values.\n  - `items`, when present, is the **complete** list. Items with the `id` of an existing item of this workout are updated; an item without an id updates a current item with the same label and exercise (else the same exercise), so re-sending a plan keeps its item ids (the phone may have cached them); other items are created; existing items left out are deleted with their sets. Omit `items` to leave them alone.\n  - Updated items merge too: omitted item fields keep their values.\n  - `sets` inside an item, when present, is its complete set list (same rules). Omit `sets` to keep them.\n  - Array order sets `position` (1, 2, 3 …) unless you send `position`.\n  - Logged work is protected: deleting a done set or an item with done sets or a logged result, undoing a done set, overwriting a set that changed since you read it (`updated_at`), or changing the exercise of an item with done sets (PUT or item PATCH) is a 409 `would_lose_logged_sets` unless `?force=1`. On an in-progress or completed workout use the item and set PATCHes instead.\n- `PATCH /api/workouts/{id}` changes workout fields only. `PATCH /api/items/{id}` and `PATCH /api/sets/{id}` merge the same way; an item PATCH may carry a complete `sets` list.\n- A `GET` response can be sent back to `PUT` unchanged (read-only fields are ignored). Editing an item's `prescription` in it re-parses the targets and cue (the stale copies next to it are ignored); editing its `exercise` name swaps the exercise and the item's name. Set ids copied from `last.sets` belong to another item: a 409.\n- Every write returns the updated resource. Unknown fields are ignored.\n\n## Ids and safe retries\n\nEvery create accepts a client-generated `id` (workouts, items, sets, programs, exercises) matching `^[A-Za-z0-9_-]{1,64}$`; otherwise the server makes one (`w_…`, `i_…`, `s_…`, `p_…`, and a slug of the name for exercises). Choosing ids up front (`w_2026-09-28_chest`) lets you refer to a workout without reading the response and makes retries safe: a repeated POST returns 409 instead of a duplicate, and a repeated PUT has the same result (items without ids are matched to the current ones, so their ids don't change). Ids are global per table: an item id that belongs to another workout, or a set id of another item, is a 409; the same set id twice in one body is a 400. Giving items ids too (`i_2026-09-28_chest_a1`) makes later edits exact.\n\n## Start, complete, skip, duplicate\n\n- **Start** (`POST …/start`): status → `in_progress`, `started_at` = now, `date` = body `date` or `today` (an overdue plan moves to the day it is done). Every exercise item without sets (except `text` tracking) gets sets from its suggestion: prefilled weight, reps or seconds, `done: false`. Starting again only fills items that still have no sets. A completed workout → 409 `already_completed`.\n- **Complete** (`POST …/complete`): sets never marked done are deleted (`discard_undone`, default true); each item becomes `done` (a done set, a non-empty `result_text`, or already done) or `skipped`; status → `completed`, `completed_at` = now; `date` = body `date`, else the workout's date if it was in progress or already completed, else `today`; `log_notes` is replaced when sent. Works on a planned workout too, and repeating it keeps the original date and time.\n- **Skip** (`POST …/skip`): for plans; status → `skipped`, nothing else changes. Undo with `PATCH {\"status\": \"planned\"}`. A completed workout → 409 `already_completed`, an in-progress one → 409 `in_progress` (`?force=1` to skip anyway).\n- **Duplicate** (`POST …/duplicate`): a new planned workout with the same items and prescriptions, without sets, results or his post-session item notes. Body `{id?, title?, date?}`.\n\n## Suggestions (prefill)\n\nWhen a workout is read with prefill, each exercise item carries `last` (the most recent completed session of that exercise other than this workout that he did; a session with logged sets beats a text-only one, and skipped items don't count) and `suggestion`:\n\n- **Sets** = `target_sets`, else the length of `target_reps`, else the number of working sets last time, else 3 (1-20).\n- **Unit** = the unit of last time's working sets, else the exercise's `default_unit`.\n- **Weight** of set i = last time's working set i (the last one repeats), rounded to 0.25.\n- **Reps** of set i = `target_reps[i]`; else, with a rep range, last time's reps + 1 kept within `rep_min`-`rep_max` (`rep_max` without history); else last time's reps.\n- **Progress:** if every working set last time reached the top of last time's target (`target_reps[i]` or `rep_max`, falling back to this plan's) and no fewer sets than prescribed were done, weights go up by the step and reps restart at `target_reps[i]` or `rep_min` (`kind: progress`), except on sets whose rep target is above what he did (never heavier and more reps at once). Step = the exercise's `weight_step` when last time's unit is its `default_unit`, else 5 lb or 2.5 kg.\n- **More reps:** a rep target up to 2 above what he did at that weight keeps the weight (`progress`, e.g. \"→ 11·11·11 reps\"). **More than 2 above** (a new block: 8 → 20 reps): the weight is scaled from that session's best Epley 1RM, `e1RM / (1 + reps / 30)`, rounded down to the step and never above last time's (`kind: deload`, \"lighter weight\").\n- **Long break:** he last did it (with or without logged sets) more than 28 days before `today` → no step and no +1 rep; same weights, \"ease back in\" (`repeat`, or `progress` when the plan's own reps are higher).\n- **Fell short:** more than half the sets 2 or more reps under the minimum → same weights (`repeat`).\n- **Bodyweight** (`reps` tracking, or no weights last time): last time's reps (+1 within a range); max-rep sets (null in `target_reps`) without an `rir` target aim for one more (`progress`).\n- **Time** tracking: last time's seconds, else `target_seconds`. **Text** tracking: no sets.\n- **Logged as text only** (\"Done\", no sets): reps or hold time from the targets, weights null, `kind: repeat` (\"Last time (…) logged as text\").\n- **No history:** `kind: new`, weights null, reps from the targets.\n\nWrite prescriptions, not weights: the app shows the suggestion with its reason and he adjusts. To prescribe a specific load on purpose (a deload, a test day), put undone sets on the plan item (`sets: [{\"weight\": 55, \"unit\": \"kg\", \"reps\": 10}, …]`, no `done`) and say why in `cue` or `notes`: start keeps an item's own sets instead of the suggestion, and on a planned workout the item's `suggestion` describes them (\"Set in the plan: …\", `deload` when lighter than last time). To program progression, read `GET /api/exercises/{id}` (history, records, suggestion) or try a prescription with `GET /api/exercises/{id}/suggestion?target_reps=12,10,8`.\n\n## Dates and the today parameter\n\n`date` fields are local calendar days (`YYYY-MM-DD`); `*_at` fields are UTC timestamps. The server clock is UTC, so every endpoint accepts `?today=YYYY-MM-DD` for the local day. It drives the long-break check in suggestions, the stats windows (this week, streak, 7- and 30-day counts) and the default `date` of start and complete. An invalid value is a 400, including a day that doesn't exist (`2026-09-31`, `2027-02-29`) in any date field.\n\n## Units and derived numbers\n\nWeights are stored exactly as entered, with `unit` `kg` or `lb` on each set (machines and cables are often in lb). Never convert when writing. Fields ending in `_kg` (`volume_kg`, `best_weight_kg`, `top_weight_kg`, `e1rm_kg` …) are converted (lb × 0.45359237) and rounded to 0.1 kg (except an item's `best`, which is exact for record checks); stats totals to 1 kg. e1RM is Epley, `weight × (1 + reps / 30)`, for sets of 1-12 reps. Volume is Σ weight × reps over done, non-warm-up sets.\n\n## Limits and validation\n\n| Field | Limit |\n|---|---|\n| ids | `^[A-Za-z0-9_-]{1,64}$` |\n| `title`, `name`, `exercise`, `external_id` | 200 characters |\n| exercise `aliases` | 50 names of up to 200 characters |\n| item `label`, `prescription`, `cue`, `result_text`, `notes`; exercise `notes` | 5,000 characters |\n| set `notes` | 2,000 characters |\n| `warmup`, workout `notes`, `log_notes`, program `description` | 20,000 characters |\n| items per workout body | 100 |\n| sets per item | 50 |\n| `target_reps` | 50 entries, integers or null |\n| `weight` | 0-2,000 |\n| `reps` | integer 0-1,000 |\n| `seconds` and item targets | integer 0-86,400 |\n| set `rir` | 0-20 |\n| `weight_step` | 0-100 |\n| `position` | workouts: integer ±1,000,000; items and sets: 0-1,000 |\n| `GET /api/workouts` | `limit` 1-200, default 50 |\n| `POST /api/parse` | 5,000 characters per text |\n\nNumbers may be sent as plain decimal strings (`\"12.5\"`, also inside `target_reps`); `\"0x14\"`, `\"1e1\"` and the like are a 400, and a blank string means null. Booleans must be JSON booleans; enums are case-sensitive; dates must be real calendar days. Unknown fields are ignored.\n\n## Errors\n\nErrors are JSON with a 4xx or 5xx status: `{\"error\": {\"code\": \"…\", \"message\": \"…\", \"details\": …}}`. The message names the problem and where it is (`items[1].sets[0].reps must be an integer`, `items[2] needs exercise_id, exercise or name`).\n\n| Status | code | When |\n|---|---|---|\n| 400 | `bad_request` | Invalid JSON, a field of the wrong type or out of range, a bad date, a missing title/name, `items` on PATCH, a set id used twice in one body |\n| 404 | `not_found` | Unknown id, or no such route |\n| 409 | `conflict` | The id, external_id, exercise name or alias already exists; an item or set id belongs to another workout or item |\n| 409 | `already_completed` | Starting or skipping a completed workout |\n| 409 | `in_progress` | Skipping a workout in progress (`force=1` to skip anyway) |\n| 409 | `would_lose_logged_sets` | A PUT would delete or undo logged sets or results, overwrite a set changed since it was read, or move done sets to another exercise (also an item PATCH); `details.losses` lists them (`force=1` to replace anyway) |\n| 422 | `unknown_exercise` | `exercise_id` does not exist |\n| 422 | `unknown_program` | `program_id` does not exist |\n| 422 | `invalid_reference` | Another broken reference |\n| 500 | `internal` | Unexpected; the message says what failed |\n\n## HTTP\n\nNo authentication and open CORS: anyone can read and write, so never put secrets in notes. Send JSON bodies with `Content-Type: application/json`. GET responses are `Cache-Control: no-store`."},"servers":[{"url":"https://gym.abdellatif.io"}],"externalDocs":{"description":"Reference with recipes","url":"https://gym.abdellatif.io/api/docs"},"tags":[{"name":"Plan","description":"What is next and what is queued."},{"name":"Workouts","description":"Plans and history, with nested items and sets, plus the start / complete / skip actions."},{"name":"Items and sets","description":"Edit one item (exercise in a workout) or one set."},{"name":"Exercises","description":"The exercise library: history, records and suggestions."},{"name":"Programs","description":"Optional groupings of workouts."},{"name":"Data","description":"Stats, export, import and the text parser."},{"name":"Discovery","description":"This documentation."}],"paths":{"/api/next":{"get":{"operationId":"getNext","tags":["Plan"],"summary":"The next workout (prefilled) and the planned queue","description":"`workout` is the `in_progress` workout (latest started), else the first `planned` workout by `date` (unscheduled last), then `position`, then creation time. Past dates stay eligible, so an overdue plan remains next until it is started, skipped, rescheduled or deleted. `upcoming` lists the other planned workouts in the same order (at most 10).","parameters":[{"$ref":"#/components/parameters/today"}],"responses":{"200":{"description":"The next workout and the queue. `workout` is null when nothing is planned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NextResponse"},"example":{"workout":{"id":"w_486229e6a086","program_id":"p_4wk_sep_2026","title":"Week 1/4","date":"2026-09-07","status":"planned","position":0,"warmup":"2-3 Min Blood Flow on any machine @EZ pace","notes":"Push Day","log_notes":null,"started_at":null,"completed_at":null,"source":"coachrx","external_id":"workout-486229e6a086","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","items":[{"target_seconds":null,"rir":null,"cue":"Go down in 3 sec","result_text":null,"notes":null,"external_id":"exercise-5dcb10ef751b","id":"i_5dcb10ef751b","workout_id":"w_486229e6a086","exercise_id":"bench-press","position":1,"label":"A1","name":"Bench Press","prescription":"3 x 12-10-8 Reps, rest 1-2 min\n* Go down in 3 sec*","target_sets":3,"target_reps":[12,10,8],"rep_min":8,"rep_max":12,"rest_seconds":90,"status":"pending","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","sets":[],"exercise":{"id":"bench-press","name":"Bench Press","aliases":[],"muscle_group":"chest","secondary_muscles":["triceps","shoulders"],"equipment":"barbell","tracking":"weight_reps","per_side":false,"default_unit":"kg","weight_step":2.5,"notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},"last":{"workout_id":"w_82bdd0102ef5","workout_title":"Chest + Biceps + Core","date":"2026-07-10","item_id":"i_f9ef7b68d2cb","prescription":"3 x 8-8-8 reps, rest 30-60s","result_text":"50kg x 10\n55kg x 8\n60kg x8","sets":[{"id":"s_f9ef7b68d2cb_1","item_id":"i_f9ef7b68d2cb","position":1,"weight":50,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-07-10T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_f9ef7b68d2cb_2","item_id":"i_f9ef7b68d2cb","position":2,"weight":55,"unit":"kg","reps":8,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-07-10T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_f9ef7b68d2cb_3","item_id":"i_f9ef7b68d2cb","position":3,"weight":60,"unit":"kg","reps":8,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-07-10T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"}]},"suggestion":{"sets":[{"weight":50,"unit":"kg","reps":12,"seconds":null},{"weight":55,"unit":"kg","reps":10,"seconds":null},{"weight":60,"unit":"kg","reps":8,"seconds":null}],"reason":"Last done 11 weeks ago (50 kg × 10, 55 kg × 8, 60 kg × 8) — same weight to ease back in, 12·10·8 reps as planned","kind":"progress"},"best":{"weight_kg":60,"e1rm_kg":76,"reps":15}}]},"upcoming":[{"id":"w_2026-09-28_chest","program_id":"p_oct_2026","title":"Chest + Biceps + Core","date":"2026-09-28","status":"planned","position":0,"warmup":null,"notes":"Week 1/4. The last set should always be challenging.","log_notes":null,"started_at":null,"completed_at":null,"source":"api","external_id":null,"created_at":"2026-09-24T09:45:17.363Z","updated_at":"2026-09-24T09:45:17.363Z","item_count":9,"set_count":0,"done_set_count":0,"volume_kg":0,"muscle_groups":["chest","biceps","core"],"exercise_names":["Incline Bench Press","Alternating Dumbbell Curl","Hollow Body Hold","Dumbbell Fly","Barbell Curl","Med Ball Russian Twist","Hand Release Push-Up","Single Arm Dumbbell Zottman Curl","Hanging Oblique Raises"]}]}}}}}}},"/api/workouts":{"get":{"operationId":"listWorkouts","tags":["Workouts"],"summary":"List workouts (summaries, no items)","description":"Newest first by default; when `status` lists only `planned` and/or `in_progress` (the queue), oldest first. Unscheduled workouts (no date) sort last in ascending order and first in descending order. Date filters exclude unscheduled workouts.","parameters":[{"name":"status","in":"query","required":false,"description":"Comma list of statuses, e.g. `planned,in_progress`.","schema":{"type":"string"},"example":"completed"},{"name":"from","in":"query","required":false,"description":"Only dates on or after this day.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Only dates on or before this day.","schema":{"type":"string","format":"date"}},{"name":"program_id","in":"query","required":false,"description":"Only this program's workouts.","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Substring of the title or of an item name (case-insensitive, literal: `%` and `_` are not wildcards), e.g. `hip thrust`.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size, 1-200 (larger is capped at 200; 0 or a non-integer is a 400). Default 50.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},{"name":"offset","in":"query","required":false,"description":"Rows to skip.","schema":{"type":"integer","minimum":0,"default":0}},{"name":"order","in":"query","required":false,"description":"By date, then position.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"One page of summaries and the total count.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutList"},"example":{"workouts":[{"id":"w_0f676309a14e","program_id":"p_split_2026","title":"Chest + Biceps + Core","date":"2026-08-24","status":"completed","position":0,"warmup":"2-3 Min Blood Flow on any machine @EZ pace\n\n2x\n10 Banded Pull-aparts\n5 Slow Prone T on Bench\n20s High Plank","notes":null,"log_notes":null,"started_at":null,"completed_at":"2026-08-24T12:00:00.000Z","source":"coachrx","external_id":"workout-0f676309a14e","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","item_count":9,"set_count":17,"done_set_count":17,"volume_kg":4285,"muscle_groups":["chest","biceps","core"],"exercise_names":["Incline Bench Press","Alternating Dumbbell Curl","Hollow Body Hold","Dumbbell Fly","Barbell Curl","Med Ball Russian Twist","Hand Release Push-Up","Single Arm Dumbbell Zottman Curl","Hanging Oblique Raises"]}],"total":73,"limit":1,"offset":0}}}},"400":{"$ref":"#/components/responses/BadRequest"}}},"post":{"operationId":"createWorkout","tags":["Workouts"],"summary":"Create a workout with nested items (and sets) in one call","description":"Atomic: everything is saved or nothing is. `status` defaults to `planned`, `source` to `api`, `position` to 0. Items reference exercises by `exercise` (name) or `exercise_id`; unknown names create exercises, and the response lists them in `created_exercises` (check it for typos). Targets are parsed from each `prescription`. Leave `sets` out of plans. 409 when the `id` or `external_id` already exists, so a retried POST never duplicates. Created as `completed` (a past session), new sets default to `done: true` and items to `done` (with sets or a result) or `skipped`.","parameters":[{"$ref":"#/components/parameters/today"}],"requestBody":{"required":true,"description":"The workout, with `items` in order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutCreate"},"example":{"id":"w_2026-09-28_chest","program_id":"p_oct_2026","title":"Chest + Biceps + Core","date":"2026-09-28","notes":"Week 1/4. The last set should always be challenging.","items":[{"label":"A1","exercise":"Incline Bench Press","prescription":"3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},{"label":"A2","exercise":"Alternating Dumbbell Curl","prescription":"3 x 14-12-10 reps, rest 30-60s"},{"label":"A3","exercise":"Hollow Body Hold","prescription":"3x 30-40s, rest as needed"},{"label":"B1","exercise":"Dumbbell Fly","prescription":"3 x 10-12 reps, rest 30-60s\n*Slow and Controlled*"},{"label":"B2","exercise":"Barbell Curl","prescription":"3 x 8-10 reps, rest 30-60s"},{"label":"B3","exercise":"Med Ball Russian Twist","prescription":"3x 30-40s, rest as needed"},{"label":"C1","exercise":"Hand Release Push-Up","prescription":"3 x Max reps (-2), rest 30-60s\n*Leave 2 reps in the tank*"},{"label":"C2","exercise":"Single Arm Dumbbell Zottman Curl","prescription":"3 x 8-10 reps/side, rest 30-60s"},{"label":"C3","exercise":"Hanging Oblique Raises","prescription":"3x 30-40s, rest as needed"}]}}}},"responses":{"201":{"description":"The created workout with prefill (this example: the first of its nine items).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"},"example":{"id":"w_2026-09-28_chest","program_id":"p_oct_2026","title":"Chest + Biceps + Core","date":"2026-09-28","status":"planned","position":0,"warmup":null,"notes":"Week 1/4. The last set should always be challenging.","log_notes":null,"started_at":null,"completed_at":null,"source":"api","external_id":null,"created_at":"2026-09-24T09:45:17.363Z","updated_at":"2026-09-24T09:45:17.363Z","items":[{"target_seconds":null,"rir":null,"cue":"Go down in 3 sec","result_text":null,"notes":null,"external_id":null,"id":"i_g1txsse06","workout_id":"w_2026-09-28_chest","exercise_id":"incline-bench-press","position":1,"label":"A1","name":"Incline Bench Press","prescription":"3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*","target_sets":3,"target_reps":[12,10,8],"rep_min":8,"rep_max":12,"rest_seconds":45,"status":"pending","created_at":"2026-09-24T09:45:17.363Z","updated_at":"2026-09-24T09:45:17.363Z","sets":[],"exercise":{"id":"incline-bench-press","name":"Incline Bench Press","aliases":[],"muscle_group":"chest","secondary_muscles":["shoulders","triceps"],"equipment":"barbell","tracking":"weight_reps","per_side":false,"default_unit":"kg","weight_step":2.5,"notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},"last":{"workout_id":"w_0f676309a14e","workout_title":"Chest + Biceps + Core","date":"2026-08-24","item_id":"i_87d562828dc0","prescription":"3 x 10-10-8 reps, rest 30-60s","result_text":"60kg x 12-10-10","sets":[{"id":"s_87d562828dc0_1","item_id":"i_87d562828dc0","position":1,"weight":60,"unit":"kg","reps":12,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_2","item_id":"i_87d562828dc0","position":2,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_3","item_id":"i_87d562828dc0","position":3,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"}]},"suggestion":{"sets":[{"weight":60,"unit":"kg","reps":12,"seconds":null},{"weight":60,"unit":"kg","reps":10,"seconds":null},{"weight":60,"unit":"kg","reps":8,"seconds":null}],"reason":"Last done 4 weeks ago (60 kg × 12·10·10) — same weight to ease back in","kind":"repeat"},"best":{"weight_kg":60,"e1rm_kg":84,"reps":12}}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/api/workouts/{id}":{"get":{"operationId":"getWorkout","tags":["Workouts"],"summary":"One workout with items and sets","description":"Items carry `exercise`, `last`, `suggestion` and `best` unless `prefill=0`. The response (with or without prefill) is valid input for PUT.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_0f676309a14e"},{"name":"prefill","in":"query","required":false,"description":"`0` leaves out `exercise`, `last`, `suggestion` and `best` on items: smaller and faster, the right choice before editing. Default `1`.","schema":{"type":"string","enum":["0","1"]}},{"$ref":"#/components/parameters/today"}],"responses":{"200":{"description":"The workout (this example: `prefill=0`, two of its nine items).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"},"example":{"id":"w_0f676309a14e","program_id":"p_split_2026","title":"Chest + Biceps + Core","date":"2026-08-24","status":"completed","position":0,"warmup":"2-3 Min Blood Flow on any machine @EZ pace\n\n2x\n10 Banded Pull-aparts\n5 Slow Prone T on Bench\n20s High Plank","notes":null,"log_notes":null,"started_at":null,"completed_at":"2026-08-24T12:00:00.000Z","source":"coachrx","external_id":"workout-0f676309a14e","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","items":[{"target_seconds":null,"rir":null,"cue":null,"result_text":"60kg x 12-10-10","notes":null,"external_id":"exercise-87d562828dc0","id":"i_87d562828dc0","workout_id":"w_0f676309a14e","exercise_id":"incline-bench-press","position":1,"label":"A1","name":"Incline Bench Press","prescription":"3 x 10-10-8 reps, rest 30-60s","target_sets":3,"target_reps":[10,10,8],"rep_min":8,"rep_max":10,"rest_seconds":45,"status":"done","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","sets":[{"id":"s_87d562828dc0_1","item_id":"i_87d562828dc0","position":1,"weight":60,"unit":"kg","reps":12,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_2","item_id":"i_87d562828dc0","position":2,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_3","item_id":"i_87d562828dc0","position":3,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"}]},{"target_seconds":40,"rir":null,"cue":null,"result_text":"Done","notes":null,"external_id":"exercise-b79178d0c772","id":"i_b79178d0c772","workout_id":"w_0f676309a14e","exercise_id":"hollow-body-hold","position":3,"label":"A3","name":"Hollow Body Hold","prescription":"3x 30-40s, rest as needed","target_sets":3,"target_reps":null,"rep_min":null,"rep_max":null,"rest_seconds":null,"status":"done","created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","sets":[]}]}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"operationId":"putWorkout","tags":["Workouts"],"summary":"Create or update a workout by id (upsert with merge rules)","description":"Keyed by the URL id: creates the workout (201; `title` required) or updates it (200). Merge rules:\n\n- Workout fields you omit keep their values.\n- `items`, when present, is the complete list: items whose `id` belongs to this workout are updated; an item without an id updates a current item with the same label and exercise (else one with the same exercise), so re-sending the same plan keeps its item ids; other items are created; existing items left out are deleted with their sets. Omit `items` to keep them.\n- Updated items merge: omitted item fields keep their values. A new `prescription` is parsed again; target fields sent alongside override the parsed values, except ones that merely repeat the stored values (so editing `prescription` in a GET response works). On a GET response, changing `exercise` (while `exercise_id` still holds the old one) swaps the exercise and its name.\n- `sets` inside an item, when present, is that item's complete set list (same rules). Omit it to keep the sets. A set id used twice is a 400; one of another item is a 409.\n- Array order sets `position` unless you send `position`.\n- **Logged work is protected:** a PUT that would delete a done set, an item with done sets or a logged result, mark a done set not done, overwrite a set that changed since you read it (its `updated_at` differs), or point an item with done sets at another exercise (they would count for an exercise he never did) is refused with 409 `would_lose_logged_sets`, listing what would be lost. For an in-progress or completed workout, change single items and sets with PATCH instead, and add a new item for a new exercise; add `force=1` only when he asked to replace them.\n- Items a PUT adds to an in-progress workout without `sets` get their suggested sets (`done: false`), like `POST …/items`.\n\nThe body `id`, if sent, must equal the URL id. An `external_id` owned by another workout is a 409.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-10-02_legs"},{"$ref":"#/components/parameters/today"},{"name":"force","in":"query","required":false,"description":"`1` = replace even if logged sets or results are deleted or changed.","schema":{"type":"string","enum":["1"]}}],"requestBody":{"required":true,"description":"Fields to set. Same shape as POST; a GET response also works.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutInput"},"example":{"title":"Lower Body","notes":"Week 1/4. No overhead pressing this week (shoulder).","items":[{"label":"A1","exercise":"One and a Quarter Goblet Squat","prescription":"3 x 12-10-8 reps, rest 30-60s\n*Go down in 3 sec*"},{"label":"A2","exercise":"Prone Hamstring Curl Machine","prescription":"3 x 12-10-8 reps, rest 30-60s"},{"label":"B1","exercise":"Dumbbell Walking Lunge","prescription":"3 x 16-14-12 alternating reps, rest 30-60s"},{"label":"B2","exercise":"Leg Extension Machine","prescription":"3 x 12-15 reps, rest 30-60s"},{"label":"C1","exercise":"Dumbbell Hip Thrust","prescription":"3 x 10-12 reps @2 sec pause at the top"},{"label":"C2","exercise":"Leg Press Machine Calf Raises","prescription":"3 x 15-20 reps, rest 30-60s\n*Pause 1 sec at the bottom*"}]}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}},"patch":{"operationId":"patchWorkout","tags":["Workouts"],"summary":"Change workout fields (date, position, title, notes, status …)","description":"Only workout fields; a body with `items` is rejected. `date: null` unschedules. Setting `status` keeps timestamps consistent (`in_progress` sets `started_at`; `completed` sets `completed_at` and a missing date; back to `planned` clears both) but does not touch sets or item statuses: use the start / complete / skip actions for real transitions.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-09-30_back"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":true,"description":"Fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutPatch"},"example":{"date":"2026-10-01"}}}},"responses":{"200":{"description":"The workout with items, sets and prefill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}},"delete":{"operationId":"deleteWorkout","tags":["Workouts"],"summary":"Delete a workout with its items and sets","description":"Permanent. Works on any status; don't delete history unless he asks. To drop a plan but keep a trace, skip it instead.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-10-05_chest"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"},"example":{"deleted":true,"id":"w_2026-10-05_chest"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workouts/{id}/start":{"post":{"operationId":"startWorkout","tags":["Workouts"],"summary":"Start: status in_progress, sets materialised from the suggestions","description":"Sets `status: in_progress`, `started_at` = now and `date` = body `date` or `today` (an overdue plan moves to the day it is done). Every exercise item without sets (except `text` tracking) gets its suggested sets with `done: false`. Calling it again on an in-progress workout (or two calls at once) only fills items that still have no sets. The app calls this; agents rarely need it.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-09-28_chest"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":false,"description":"Optional.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartInput"},"example":{"date":"2026-09-28"}}}},"responses":{"200":{"description":"The started workout with its new sets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/workouts/{id}/complete":{"post":{"operationId":"completeWorkout","tags":["Workouts"],"summary":"Complete: status completed, unlogged sets dropped, items marked done/skipped","description":"Deletes sets never marked done (unless `discard_undone: false`); marks each item `done` (it has a done set, a non-empty `result_text`, or was already done) or `skipped`; sets `status: completed` and `completed_at`; `date` = body `date`, else the workout's date if it was in progress or already completed, else `today`; replaces `log_notes` when sent (null or `\"\"` clears it). Works on a planned workout too. Repeating it keeps the original date and completion time.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-09-28_chest"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":false,"description":"Optional.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteInput"},"example":{"discard_undone":true,"log_notes":"Felt strong. Left shoulder fine."}}}},"responses":{"200":{"description":"The completed workout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workouts/{id}/skip":{"post":{"operationId":"skipWorkout","tags":["Workouts"],"summary":"Skip a planned workout (restore with PATCH status planned)","description":"For plans. Only the status changes; the workout keeps its items and date. Restore with `PATCH {\"status\": \"planned\"}`. The response has no prefill. A completed workout is a 409 `already_completed` and an in-progress one a 409 `in_progress` (skipping would drop logged work from his history); `force=1` skips it anyway, e.g. a session abandoned days ago.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_486229e6a086"},{"name":"force","in":"query","required":false,"description":"`1` = skip even a completed or in-progress workout.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"The skipped workout (no prefill).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/workouts/{id}/duplicate":{"post":{"operationId":"duplicateWorkout","tags":["Workouts"],"summary":"Copy a workout as a new plan (items and prescriptions, no sets)","description":"Copies title, program, warm-up, notes and every item (exercise, label, prescription, targets, cue). Item notes are copied only from a planned workout (without the app's `Swapped from …` / `Skipped: …` lines): on a done session they are his log. Results, sets and log notes are not copied. The copy is `planned`, with `source: api` unless the body sets `source`.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout to copy id.","schema":{"type":"string"},"example":"w_2026-09-28_chest"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":false,"description":"Optional.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateInput"},"example":{"id":"w_2026-10-05_chest","date":"2026-10-05"}}}},"responses":{"201":{"description":"The copy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/workouts/{id}/reorder":{"post":{"operationId":"reorderItems","tags":["Workouts"],"summary":"Reorder a workout's items","description":"`item_ids` must list every item of the workout exactly once. Positions become 1…n in that order.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-09-28_chest"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":true,"description":"The new order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderInput"},"example":{"item_ids":["i_g1txsse06","i_hshn07zli","i_3fr4bg06e"]}}}},"responses":{"200":{"description":"The workout with items, sets and prefill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutDetail"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/workouts/{id}/items":{"post":{"operationId":"addItem","tags":["Items and sets"],"summary":"Add an item (exercise or free-form block) to a workout","description":"Appended after the last item unless `position` is sent (other items are not shifted: reorder afterwards if needed). Needs `exercise`, `exercise_id` or `name`. When the workout is in progress and no `sets` are sent, suggested sets are created. Set ids that already exist are a 409.","parameters":[{"name":"id","in":"path","required":true,"description":"Workout id.","schema":{"type":"string"},"example":"w_2026-09-28_chest"},{"$ref":"#/components/parameters/today"}],"requestBody":{"required":true,"description":"The item.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemInput"},"example":{"id":"i_2026-09-28_chest_d","label":"D","exercise":"Dumbbell Hammer Curl","prescription":"2 x 15-20 reps, rest 30s\n*Squeeze at the top*"}}}},"responses":{"201":{"description":"The created item with prefill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/api/items/{id}":{"patch":{"operationId":"updateItem","tags":["Items and sets"],"summary":"Change an item (prescription, exercise, label, notes …)","description":"Merges: omitted fields keep their values. A new `prescription` is parsed into targets and cue (fields sent alongside override); clearing it clears them. Changing `exercise`/`exercise_id` also renames the item to the exercise's name unless `name` is sent; on an item with done sets it is a 409 `would_lose_logged_sets` (they stay with the exercise he did: add a new item for the new one), unless `force=1`. `sets`, when sent, is the complete set list, merged like PUT: a set id used twice is a 400, and one that belongs to another item (e.g. copied from `last.sets`) is a 409, never moved or overwritten.","parameters":[{"name":"id","in":"path","required":true,"description":"Item id.","schema":{"type":"string"},"example":"i_g1txsse06"},{"$ref":"#/components/parameters/today"},{"name":"force","in":"query","required":false,"description":"`1` = change the exercise even though the item has done sets (they move with it).","schema":{"type":"string","enum":["1"]}}],"requestBody":{"required":true,"description":"Fields to change (`id` is ignored).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemInput"},"example":{"prescription":"3 x 12-10-8 reps, rest 30-60s\n*Slow and controlled*"}}}},"responses":{"200":{"description":"The item with prefill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutItem"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}},"delete":{"operationId":"deleteItem","tags":["Items and sets"],"summary":"Delete an item and its sets","parameters":[{"name":"id","in":"path","required":true,"description":"Item id.","schema":{"type":"string"},"example":"i_2026-09-28_chest_d"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"},"example":{"deleted":true,"id":"i_2026-09-28_chest_d"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/items/{id}/sets":{"post":{"operationId":"addSet","tags":["Items and sets"],"summary":"Add a set (defaults copied from the previous set)","description":"Weight, unit, reps and seconds default to the item's last set; `done` and `is_warmup` default to false. Appended after the last set. An `id` that already exists is a 409.","parameters":[{"name":"id","in":"path","required":true,"description":"Item id.","schema":{"type":"string"},"example":"i_87d562828dc0"}],"requestBody":{"required":false,"description":"Values that differ from the previous set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetInput"},"example":{"reps":8,"done":true}}}},"responses":{"201":{"description":"The created set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutSet"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/sets/{id}":{"patch":{"operationId":"updateSet","tags":["Items and sets"],"summary":"Change a set (e.g. log it: done true)","parameters":[{"name":"id","in":"path","required":true,"description":"Set id.","schema":{"type":"string"},"example":"s_87d562828dc0_1"}],"requestBody":{"required":true,"description":"Fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetInput"},"example":{"weight":62.5,"reps":10,"done":true}}}},"responses":{"200":{"description":"The set.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkoutSet"},"example":{"id":"s_87d562828dc0_1","item_id":"i_87d562828dc0","position":1,"weight":60,"unit":"kg","reps":12,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteSet","tags":["Items and sets"],"summary":"Delete a set","parameters":[{"name":"id","in":"path","required":true,"description":"Set id.","schema":{"type":"string"},"example":"s_87d562828dc0_3"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/exercises":{"get":{"operationId":"listExercises","tags":["Exercises"],"summary":"List exercises with usage stats (search before naming one in a plan)","parameters":[{"name":"q","in":"query","required":false,"description":"Words to find in the name or an alias: every word must appear, ignoring case, spaces, hyphens, apostrophes and a plural s (`pull up` finds Strict Pull-Up, `triceps` finds Rope Tricep Pushdown). `db`, `bb`, `kb`, `rdl` and `ohp` are understood.","schema":{"type":"string"},"example":"curl"},{"name":"muscle_group","in":"query","required":false,"description":"Primary or secondary muscle group.","schema":{"$ref":"#/components/schemas/MuscleGroup"}},{"name":"equipment","in":"query","required":false,"description":"Equipment.","schema":{"$ref":"#/components/schemas/Equipment"}},{"name":"sort","in":"query","required":false,"description":"`recent` (default: last performed first), `name`, or `frequency` (most performed first).","schema":{"type":"string","enum":["recent","name","frequency"]}}],"responses":{"200":{"description":"All matches (no paging).","content":{"application/json":{"schema":{"type":"object","properties":{"exercises":{"type":"array","items":{"$ref":"#/components/schemas/ExerciseSummary"}}},"required":["exercises"]},"example":{"exercises":[{"id":"incline-bench-press","name":"Incline Bench Press","aliases":[],"muscle_group":"chest","secondary_muscles":["shoulders","triceps"],"equipment":"barbell","tracking":"weight_reps","per_side":false,"default_unit":"kg","weight_step":2.5,"notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","times_performed":8,"last_performed":"2026-08-24","best_weight_kg":60,"best_e1rm_kg":84}]}}}},"400":{"$ref":"#/components/responses/BadRequest"}}},"post":{"operationId":"createExercise","tags":["Exercises"],"summary":"Create an exercise with explicit metadata","description":"Items can also create exercises implicitly by name; create one explicitly to set the unit, step or tracking. 409 when the id or name exists, or when the name or an alias is already another exercise's name or alias.","requestBody":{"required":true,"description":"The exercise.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExerciseInput"},"example":{"name":"Cable Crossover","muscle_group":"chest","secondary_muscles":["shoulders"],"equipment":"cable","tracking":"weight_reps","default_unit":"lb","weight_step":5}}}},"responses":{"201":{"description":"The created exercise.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Exercise"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/exercises/{id}":{"get":{"operationId":"getExercise","tags":["Exercises"],"summary":"Exercise detail: full history, PRs and next suggestion","parameters":[{"name":"id","in":"path","required":true,"description":"Exercise id.","schema":{"type":"string"},"example":"incline-bench-press"},{"$ref":"#/components/parameters/today"}],"responses":{"200":{"description":"The exercise with its history (newest first), records and suggestion.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExerciseDetail"},"example":{"id":"incline-bench-press","name":"Incline Bench Press","aliases":[],"muscle_group":"chest","secondary_muscles":["shoulders","triceps"],"equipment":"barbell","tracking":"weight_reps","per_side":false,"default_unit":"kg","weight_step":2.5,"notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z","history":[{"workout_id":"w_0f676309a14e","workout_title":"Chest + Biceps + Core","date":"2026-08-24","item_id":"i_87d562828dc0","label":"A1","prescription":"3 x 10-10-8 reps, rest 30-60s","result_text":"60kg x 12-10-10","sets":[{"id":"s_87d562828dc0_1","item_id":"i_87d562828dc0","position":1,"weight":60,"unit":"kg","reps":12,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_2","item_id":"i_87d562828dc0","position":2,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"},{"id":"s_87d562828dc0_3","item_id":"i_87d562828dc0","position":3,"weight":60,"unit":"kg","reps":10,"seconds":null,"rir":null,"is_warmup":false,"done":true,"done_at":"2026-08-24T12:00:00.000Z","notes":null,"created_at":"2026-09-24T00:00:00.000Z","updated_at":"2026-09-24T00:00:00.000Z"}],"top_weight_kg":60,"best_e1rm_kg":84,"volume_kg":1920,"total_reps":32,"status":"done","notes":null,"cue":null,"target_sets":3,"target_reps":[10,10,8],"rep_min":8,"rep_max":10,"target_seconds":null,"rest_seconds":45,"rir":null}],"records":{"heaviest":{"weight":60,"unit":"kg","reps":10,"date":"2026-07-21"},"best_e1rm":{"e1rm_kg":84,"weight":60,"unit":"kg","reps":12,"date":"2026-08-24"},"most_reps":{"reps":12,"weight":40,"unit":"kg","date":"2026-04-29"},"best_volume":{"volume_kg":1920,"date":"2026-08-24"}},"suggestion":{"sets":[{"weight":60,"unit":"kg","reps":10,"seconds":null},{"weight":60,"unit":"kg","reps":10,"seconds":null},{"weight":60,"unit":"kg","reps":8,"seconds":null}],"reason":"Last done 4 weeks ago (60 kg × 12·10·10) — same weight to ease back in","kind":"repeat"}}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateExercise","tags":["Exercises"],"summary":"Change an exercise (aliases, notes, unit, step …)","description":"`aliases` replaces the whole list. Renaming keeps the old name as an alias (unless `aliases` is sent), because past items still carry it. Renaming to another exercise's name, or adding an alias that is another exercise's name or alias, is a 409 that names that exercise (merge duplicates instead).","parameters":[{"name":"id","in":"path","required":true,"description":"Exercise id.","schema":{"type":"string"},"example":"cable-crossover"}],"requestBody":{"required":true,"description":"Fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExercisePatch"},"example":{"aliases":["Standing Cable Fly","Cable Crossovers"]}}}},"responses":{"200":{"description":"The exercise.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Exercise"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"}}},"delete":{"operationId":"deleteExercise","tags":["Exercises"],"summary":"Delete an exercise (items keep their name, lose the link)","description":"Items that used it keep their name but get `exercise_id: null`, which cuts them out of history and suggestions. To fix a duplicate, merge instead.","parameters":[{"name":"id","in":"path","required":true,"description":"Exercise id.","schema":{"type":"string"},"example":"cable-crossover"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/exercises/{id}/merge":{"post":{"operationId":"mergeExercise","tags":["Exercises"],"summary":"Merge a duplicate into another exercise (history moves, name becomes an alias)","description":"Every item of `{id}` moves to `into`; `{id}`'s name and aliases become aliases of `into`; `{id}` is deleted.","parameters":[{"name":"id","in":"path","required":true,"description":"Exercise to remove id.","schema":{"type":"string"},"example":"standing-cable-fly"}],"requestBody":{"required":true,"description":"The exercise to keep.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeInput"},"example":{"into":"cable-crossover"}}}},"responses":{"200":{"description":"The kept exercise.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Exercise"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/exercises/{id}/suggestion":{"get":{"operationId":"suggestExercise","tags":["Exercises"],"summary":"What the app would prefill for a prescription you are considering","description":"Pass the prescription you plan to write (`prescription`, parsed like an item's) and/or its targets; explicit targets override the parsed ones. Without any, last time's targets are used. Same rule as the prefill on workout items. Non-integer values are a 400.","parameters":[{"name":"id","in":"path","required":true,"description":"Exercise id.","schema":{"type":"string"},"example":"dumbbell-fly"},{"name":"prescription","in":"query","required":false,"description":"Coach-style text, parsed into the targets below.","schema":{"type":"string","maxLength":5000},"example":"3 x 12-10-8 reps, rest 60s"},{"name":"target_sets","in":"query","required":false,"description":"Number of sets.","schema":{"type":"integer"}},{"name":"target_reps","in":"query","required":false,"description":"Comma list of per-set reps; `max` for a max-reps set.","schema":{"type":"string"},"example":"12,10,8"},{"name":"rep_min","in":"query","required":false,"description":"Bottom of a rep range.","schema":{"type":"integer"}},{"name":"rep_max","in":"query","required":false,"description":"Top of a rep range.","schema":{"type":"integer"}},{"name":"target_seconds","in":"query","required":false,"description":"Hold time.","schema":{"type":"integer"}},{"name":"rir","in":"query","required":false,"description":"Target reps in reserve: max-rep sets then repeat last time's reps instead of \"beat it by one\".","schema":{"type":"integer"}},{"$ref":"#/components/parameters/today"}],"responses":{"200":{"description":"The suggested sets and the reason.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Suggestion"},"example":{"sets":[{"weight":15,"unit":"kg","reps":8,"seconds":null},{"weight":15,"unit":"kg","reps":8,"seconds":null},{"weight":15,"unit":"kg","reps":8,"seconds":null}],"reason":"Hit 10 reps on every set on 24 Aug → +2.5 kg","kind":"progress"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/programs":{"get":{"operationId":"listPrograms","tags":["Programs"],"summary":"List programs with workout counts","description":"Active programs first, then newest first.","responses":{"200":{"description":"Every program.","content":{"application/json":{"schema":{"type":"object","properties":{"programs":{"type":"array","items":{"$ref":"#/components/schemas/ProgramListEntry"}}},"required":["programs"]},"example":{"programs":[{"id":"p_oct_2026","name":"Hypertrophy block Oct 2026","description":"4 weeks, 3-day rotation: Chest + Biceps + Core / Back + Triceps + Core / Shoulders + Arms + Legs. Reps wave 12-10-8 → 15-12-10 → 12-10-8 → 10-10-8.","status":"active","created_at":"2026-09-24T09:45:17.344Z","updated_at":"2026-09-24T09:45:17.344Z","workout_count":3,"completed_count":0}]}}}}}},"post":{"operationId":"createProgram","tags":["Programs"],"summary":"Create a program","requestBody":{"required":true,"description":"The program.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramInput"},"example":{"id":"p_oct_2026","name":"Hypertrophy block Oct 2026","description":"4 weeks, 3-day rotation: Chest + Biceps + Core / Back + Triceps + Core / Shoulders + Arms + Legs. Reps wave 12-10-8 → 15-12-10 → 12-10-8 → 10-10-8."}}}},"responses":{"201":{"description":"The created program.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Program"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/programs/{id}":{"get":{"operationId":"getProgram","tags":["Programs"],"summary":"A program with its workouts (summaries)","parameters":[{"name":"id","in":"path","required":true,"description":"Program id.","schema":{"type":"string"},"example":"p_oct_2026"}],"responses":{"200":{"description":"The program and its workouts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramDetail"}}}},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"operationId":"updateProgram","tags":["Programs"],"summary":"Change a program (name, description, status)","parameters":[{"name":"id","in":"path","required":true,"description":"Program id.","schema":{"type":"string"},"example":"p_4wk_sep_2026"}],"requestBody":{"required":true,"description":"Fields to change.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProgramPatch"},"example":{"status":"archived"}}}},"responses":{"200":{"description":"The program.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Program"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"operationId":"deleteProgram","tags":["Programs"],"summary":"Delete a program (optionally with its planned workouts)","description":"Its workouts stay, with `program_id: null`, unless `delete_workouts=1`, which also deletes its **planned** workouts (completed and skipped ones stay).","parameters":[{"name":"id","in":"path","required":true,"description":"Program id.","schema":{"type":"string"},"example":"p_oct_2026"},{"name":"delete_workouts","in":"query","required":false,"description":"`1` also deletes the program's planned workouts.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Deleted"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/api/stats":{"get":{"operationId":"getStats","tags":["Data"],"summary":"Totals, weekly volume, streak, muscle recency, recent PRs","parameters":[{"$ref":"#/components/parameters/today"}],"responses":{"200":{"description":"Stats (this example: `today=2026-08-27`, lists shortened).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stats"},"example":{"total_workouts":73,"total_sets":1163,"total_volume_kg":295661,"this_week":{"workouts":3,"sets":41,"volume_kg":9401},"streak_weeks":3,"weekly":[{"week_start":"2026-08-10","workouts":4,"sets":65,"volume_kg":14015},{"week_start":"2026-08-17","workouts":2,"sets":39,"volume_kg":8798},{"week_start":"2026-08-24","workouts":3,"sets":41,"volume_kg":9401}],"muscle_recency":{"chest":{"last_date":"2026-08-24","days_ago":3,"sets_7d":8},"back":{"last_date":"2026-08-25","days_ago":2,"sets_7d":12},"biceps":{"last_date":"2026-08-26","days_ago":1,"sets_7d":18}},"recent_prs":[{"exercise_id":"dumbbell-fly","exercise_name":"Dumbbell Fly","kind":"heaviest","value":12.5,"unit":"kg","date":"2026-08-24"},{"exercise_id":"incline-bench-press","exercise_name":"Incline Bench Press","kind":"e1rm","value":84,"unit":"kg","date":"2026-08-24"}]}}}}}}},"/api/export":{"get":{"operationId":"exportData","tags":["Data"],"summary":"Everything as one JSON document (backup)","description":"Sent with `Content-Disposition: attachment`. The file can be sent back to POST /api/import.","responses":{"200":{"description":"Every exercise, program and workout.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportData"}}}}}}},"/api/import":{"post":{"operationId":"importData","tags":["Data"],"summary":"Bulk upsert exercises, programs and workouts","description":"Exercises upsert by id (default: slug of the name), programs by id, workouts like PUT (by id, else `external_id`, else created; `items` and `sets` are complete lists). Every row is validated first, so a malformed row (400, with its index: `workouts[3].items[1].sets[0].reps …`) writes nothing. Rows are then saved one at a time: a row that fails while saving (409, 422) stops the import with its index in the message, and earlier rows stay saved. Import restores data, so it may replace logged sets (no `would_lose_logged_sets` check). Re-importing an export into the same database changes nothing; into an empty one it restores the data, including `started_at`, `completed_at` and `done_at`.","requestBody":{"required":true,"description":"Any subset of the export shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportInput"},"example":{"workouts":[{"id":"w_2026-10-05_chest","program_id":"p_oct_2026","title":"Chest + Biceps + Core","date":"2026-10-05","notes":"Week 2/4.","items":[{"label":"A1","exercise":"Incline Bench Press","prescription":"3 x 15-12-10 reps, rest 30-60s\n*Go down in 3 sec*"},{"label":"A2","exercise":"Alternating Dumbbell Curl","prescription":"3 x 15-12-10 reps, rest 30-60s"},{"label":"A3","exercise":"Hollow Body Hold","prescription":"3x 30-40s, rest as needed"}]},{"id":"w_2026-10-07_back","program_id":"p_oct_2026","title":"Back + Triceps + Core","date":"2026-10-07","notes":"Week 2/4.","items":[{"label":"A1","exercise":"V-Bar Cable Lat Pulldown Machine","prescription":"3 x 15-12-10 reps, rest 30-60s"},{"label":"A2","exercise":"Rope Tricep Pushdown","prescription":"3 x 15-12-10 reps, rest 30-60s"},{"label":"A3","exercise":"Alternating V-Ups","prescription":"3x 16-20 Alt Reps, rest as needed"}]}]}}}},"responses":{"200":{"description":"Rows processed per table.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportResult"},"example":{"imported":{"exercises":0,"programs":0,"workouts":2}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/Unprocessable"}}}},"/api/parse":{"post":{"operationId":"parseText","tags":["Data"],"summary":"Preview how a prescription will be parsed (and how a result text reads as sets)","description":"Nothing is saved. Workout writes parse `prescription` the same way; `result_text` is stored as text and never turned into sets (send sets). Each text is at most 5000 characters.","requestBody":{"required":true,"description":"Text to parse.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseInput"},"example":{"prescription":"3 x Max Reps (-2), rest 60-90s\n*Leave 2 reps in the tank*","result":"25kg x 12 x 3"}}}},"responses":{"200":{"description":"Parsed values; null for a field not sent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ParseOutput"},"example":{"prescription":{"target_sets":3,"target_reps":[null,null,null],"rep_min":null,"rep_max":null,"target_seconds":null,"rest_seconds":75,"rir":2,"cue":null,"per_side":false,"kind":"sets"},"result":{"status":"done","sets":[{"weight":25,"unit":"kg","reps":12,"seconds":null,"is_warmup":false},{"weight":25,"unit":"kg","reps":12,"seconds":null,"is_warmup":false},{"weight":25,"unit":"kg","reps":12,"seconds":null,"is_warmup":false}],"note":null,"confidence":"high"}}}}},"400":{"$ref":"#/components/responses/BadRequest"}}}},"/api":{"get":{"operationId":"getIndex","tags":["Discovery"],"summary":"API index: resources, endpoints, links to the docs","responses":{"200":{"description":"The index.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiIndex"}}}}}}},"/api/openapi.json":{"get":{"operationId":"getOpenApi","tags":["Discovery"],"summary":"This OpenAPI 3.1 document","responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/docs":{"get":{"operationId":"getDocs","tags":["Discovery"],"summary":"Human-readable reference with recipes (HTML)","responses":{"200":{"description":"HTML page.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/llms.txt":{"get":{"operationId":"getLlmsTxt","tags":["Discovery"],"summary":"Guide for coding agents (markdown)","responses":{"200":{"description":"Markdown text.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/health":{"get":{"operationId":"getHealth","tags":["Discovery"],"summary":"Health check","responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"ok":true,"workouts":76,"time":"2026-09-24T09:36:35.578Z"}}}}}}}},"components":{"schemas":{"MuscleGroup":{"type":"string","enum":["chest","back","shoulders","biceps","triceps","forearms","core","quads","hamstrings","glutes","calves","adductors","abductors","full_body","cardio"],"description":"Muscle groups used by exercises, stats and the recovery map."},"Equipment":{"type":"string","enum":["barbell","dumbbell","ez_bar","cable","machine","bodyweight","kettlebell","med_ball","band","other"]},"Tracking":{"type":"string","enum":["weight_reps","reps","time","text"],"description":"How sets are logged: `weight_reps` (weight × reps), `reps` (bodyweight reps), `time` (seconds, e.g. planks and holds), `text` (a free-text result, no sets)."},"Unit":{"type":"string","enum":["kg","lb"],"description":"Weights are stored as entered with their unit; nothing is converted on write."},"WorkoutStatus":{"type":"string","enum":["planned","in_progress","completed","skipped"],"description":"`planned` → `in_progress` (start) → `completed` (complete); `skipped` when skipped."},"ItemStatus":{"type":"string","enum":["pending","done","skipped"],"description":"`pending` until the workout is completed; then `done` or `skipped`."},"Exercise":{"type":"object","properties":{"id":{"type":"string","description":"Slug, e.g. `incline-bench-press`."},"name":{"type":"string","description":"Canonical name; unique, case-insensitive."},"aliases":{"type":"array","items":{"type":"string"},"description":"Other names that resolve to this exercise when an item references it by name."},"muscle_group":{"anyOf":[{"$ref":"#/components/schemas/MuscleGroup"},{"type":"null"}],"description":"Primary muscle group."},"secondary_muscles":{"type":"array","items":{"$ref":"#/components/schemas/MuscleGroup"}},"equipment":{"anyOf":[{"$ref":"#/components/schemas/Equipment"},{"type":"null"}]},"tracking":{"$ref":"#/components/schemas/Tracking"},"per_side":{"type":"boolean","description":"Reps and weight are per arm or leg."},"default_unit":{"$ref":"#/components/schemas/Unit","description":"Unit used when there is no history."},"weight_step":{"type":["number","null"],"description":"Progression increment in `default_unit`. null (or a session logged in the other unit) = 2.5 kg / 5 lb."},"notes":{"type":["string","null"],"description":"His note about the exercise (e.g. availability). Read it before programming."},"created_at":{"type":"string","format":"date-time","description":"When the row was created (UTC)."},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed (UTC)."}},"required":["id","name","aliases","muscle_group","secondary_muscles","equipment","tracking","per_side","default_unit","weight_step","notes","created_at","updated_at"],"description":"A canonical movement. Items link to it, so history, records and suggestions span every workout."},"ExerciseSummary":{"allOf":[{"$ref":"#/components/schemas/Exercise"},{"type":"object","properties":{"times_performed":{"type":"integer","description":"Completed workouts where he did it (the item is done or has a done set; skipped items don't count)."},"last_performed":{"type":["string","null"],"format":"date","description":"Date of the latest of those workouts."},"best_weight_kg":{"type":["number","null"],"description":"Heaviest done working set, in kg."},"best_e1rm_kg":{"type":["number","null"],"description":"Best estimated 1RM (Epley, sets of 1-12 reps), in kg."}},"required":["times_performed","last_performed","best_weight_kg","best_e1rm_kg"]}],"description":"A row of GET /api/exercises: the exercise plus usage stats."},"ExerciseHistoryEntry":{"type":"object","properties":{"workout_id":{"type":"string"},"workout_title":{"type":"string"},"date":{"type":["string","null"],"format":"date"},"item_id":{"type":"string"},"label":{"type":["string","null"]},"prescription":{"type":["string","null"],"description":"The prescription at the time."},"result_text":{"type":["string","null"]},"sets":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSet"},"description":"Done sets only; empty for a text-only result like `Done`."},"top_weight_kg":{"type":["number","null"]},"best_e1rm_kg":{"type":["number","null"]},"volume_kg":{"type":"number"},"total_reps":{"type":"integer"},"status":{"$ref":"#/components/schemas/ItemStatus","description":"`skipped` when he skipped it that day (the reason is in `notes`)."},"notes":{"type":["string","null"],"description":"His note on the item: skip reason, question, mistake."},"cue":{"type":["string","null"]},"target_sets":{"type":["integer","null"]},"target_reps":{"type":["array","null"],"items":{"type":["integer","null"]},"description":"Only for per-set schemes (12-10-8) or max-rep sets (nulls)."},"rep_min":{"type":["integer","null"]},"rep_max":{"type":["integer","null"]},"target_seconds":{"type":["integer","null"]},"rest_seconds":{"type":["integer","null"]},"rir":{"type":["integer","null"]}},"required":["workout_id","workout_title","date","item_id","label","prescription","result_text","sets","top_weight_kg","best_e1rm_kg","volume_kg","total_reps","status","notes","cue","target_sets","target_reps","rep_min","rep_max","target_seconds","rest_seconds","rir"],"description":"One completed session of an exercise."},"PersonalRecords":{"type":"object","properties":{"heaviest":{"type":["object","null"],"properties":{"weight":{"type":"number"},"unit":{"$ref":"#/components/schemas/Unit"},"reps":{"type":["integer","null"]},"date":{"type":["string","null"],"format":"date"}},"required":["weight","unit","reps","date"]},"best_e1rm":{"type":["object","null"],"properties":{"e1rm_kg":{"type":"number"},"weight":{"type":"number"},"unit":{"$ref":"#/components/schemas/Unit"},"reps":{"type":"integer"},"date":{"type":["string","null"],"format":"date"}},"required":["e1rm_kg","weight","unit","reps","date"]},"most_reps":{"type":["object","null"],"properties":{"reps":{"type":"integer"},"weight":{"type":["number","null"]},"unit":{"$ref":"#/components/schemas/Unit"},"date":{"type":["string","null"],"format":"date"}},"required":["reps","weight","unit","date"]},"best_volume":{"type":["object","null"],"properties":{"volume_kg":{"type":"number"},"date":{"type":["string","null"],"format":"date"}},"required":["volume_kg","date"],"description":"Best single-session volume."}},"required":["heaviest","best_e1rm","most_reps","best_volume"],"description":"All-time bests over done working sets. Weights are as entered; comparisons use kg; the first date a record was set wins ties."},"ExerciseDetail":{"allOf":[{"$ref":"#/components/schemas/Exercise"},{"type":"object","properties":{"history":{"type":"array","items":{"$ref":"#/components/schemas/ExerciseHistoryEntry"},"description":"Every completed session, newest first."},"records":{"$ref":"#/components/schemas/PersonalRecords"},"suggestion":{"$ref":"#/components/schemas/Suggestion","description":"Next-session prefill, assuming last time's prescription is repeated."}},"required":["history","records","suggestion"]}],"description":"GET /api/exercises/{id}."},"Program":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"status":{"type":"string","enum":["active","archived"]},"created_at":{"type":"string","format":"date-time","description":"When the row was created (UTC)."},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed (UTC)."}},"required":["id","name","description","status","created_at","updated_at"],"description":"An optional grouping of workouts, e.g. a 4-week block."},"ProgramListEntry":{"allOf":[{"$ref":"#/components/schemas/Program"},{"type":"object","properties":{"workout_count":{"type":"integer"},"completed_count":{"type":"integer"}},"required":["workout_count","completed_count"]}],"description":"A row of GET /api/programs."},"ProgramDetail":{"allOf":[{"$ref":"#/components/schemas/Program"},{"type":"object","properties":{"workouts":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSummary"},"description":"Dated workouts by date, then unscheduled ones; ties by position."}},"required":["workouts"]}],"description":"GET /api/programs/{id}."},"WorkoutSet":{"type":"object","properties":{"id":{"type":"string"},"item_id":{"type":"string"},"position":{"type":"integer","description":"Order within the item, from 1."},"weight":{"type":["number","null"],"description":"As entered, in `unit`. null = bodyweight or not recorded."},"unit":{"$ref":"#/components/schemas/Unit"},"reps":{"type":["integer","null"]},"seconds":{"type":["integer","null"],"description":"For time-tracked exercises."},"rir":{"type":["number","null"],"description":"Reps in reserve he reported."},"is_warmup":{"type":"boolean","description":"Warm-up sets are left out of volume, records and suggestions."},"done":{"type":"boolean","description":"Performed and logged. Only done sets count as history."},"done_at":{"type":["string","null"],"format":"date-time"},"notes":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","description":"When the row was created (UTC)."},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed (UTC)."}},"required":["id","item_id","position","weight","unit","reps","seconds","rir","is_warmup","done","done_at","notes","created_at","updated_at"],"description":"One set of an item."},"WorkoutItem":{"type":"object","properties":{"id":{"type":"string"},"workout_id":{"type":"string"},"exercise_id":{"type":["string","null"],"description":"Linked exercise; null for a free-form block (circuit, AMRAP, cardio)."},"position":{"type":"integer","description":"Order in the workout, from 1."},"label":{"type":["string","null"],"description":"Superset label. Consecutive items sharing a letter form one group: `A1`, `A2`, `A3` = tri-set A; a bare `D` is a single block."},"name":{"type":"string","description":"The exercise name, or the block title of a free-form block."},"prescription":{"type":["string","null"],"description":"The plan as the coach writes it, shown verbatim, e.g. `3 x 12-10-8 reps, rest 30-60s\\n*Go down in 3 sec*`."},"target_sets":{"type":["integer","null"]},"target_reps":{"type":["array","null"],"items":{"type":["integer","null"]},"description":"Per-set rep targets, e.g. [12, 10, 8]; a null entry means max reps."},"rep_min":{"type":["integer","null"],"description":"Bottom of the rep range (for a scheme: its smallest entry)."},"rep_max":{"type":["integer","null"],"description":"Top of the rep range (for a scheme: its largest entry)."},"target_seconds":{"type":["integer","null"],"description":"Hold time per set: the top of a range (30-40s → 40)."},"rest_seconds":{"type":["integer","null"],"description":"Rest between sets or rounds; a range becomes its midpoint rounded to 15 s (30-60s → 45)."},"rir":{"type":["integer","null"],"description":"Target reps in reserve (`Max reps (-2)` → 2)."},"cue":{"type":["string","null"],"description":"Technique cue, e.g. `Go down in 3 sec`."},"status":{"$ref":"#/components/schemas/ItemStatus"},"result_text":{"type":["string","null"],"description":"Free-text result: text blocks (`Done`, `5 rounds + 12 reps`) and imported logs like `25kg x 12 x 3`."},"notes":{"type":["string","null"],"description":"His notes, e.g. `Skipped: Machine not available.` Read them when planning."},"external_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","description":"When the row was created (UTC)."},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed (UTC)."},"sets":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSet"},"description":"By position. Empty for a planned workout until it is started."},"exercise":{"anyOf":[{"$ref":"#/components/schemas/Exercise"},{"type":"null"}],"description":"With prefill only."},"last":{"anyOf":[{"$ref":"#/components/schemas/LastPerformance"},{"type":"null"}],"description":"With prefill only: the most recent completed session of this exercise."},"suggestion":{"anyOf":[{"$ref":"#/components/schemas/Suggestion"},{"type":"null"}],"description":"With prefill only: the prefill for this session; null for free-form blocks."},"best":{"type":["object","null"],"properties":{"weight_kg":{"type":["number","null"]},"e1rm_kg":{"type":["number","null"]},"reps":{"type":["integer","null"]}},"required":["weight_kg","e1rm_kg","reps"],"description":"With prefill only: bests before this workout (live PR detection)."}},"required":["id","workout_id","exercise_id","position","label","name","prescription","target_sets","target_reps","rep_min","rep_max","target_seconds","rest_seconds","rir","cue","status","result_text","notes","external_id","created_at","updated_at","sets"],"description":"One exercise (or free-form block) in a workout."},"Workout":{"type":"object","properties":{"id":{"type":"string"},"program_id":{"type":["string","null"]},"title":{"type":"string","description":"e.g. `Chest + Biceps + Core`."},"date":{"type":["string","null"],"format":"date","description":"The planned day, or the day it was done. null = unscheduled."},"status":{"$ref":"#/components/schemas/WorkoutStatus"},"position":{"type":"integer","description":"Tie-breaker among planned workouts on the same date; lower first. May be negative."},"warmup":{"type":["string","null"],"description":"Warm-up routine text."},"notes":{"type":["string","null"],"description":"Plan notes from the coach or agent, shown before starting."},"log_notes":{"type":["string","null"],"description":"His notes on how it went. Read them when planning."},"started_at":{"type":["string","null"],"format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"source":{"type":"string","enum":["app","api","coachrx"],"description":"Who created it: `api` (API writes), `app`, or `coachrx` (imported history)."},"external_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time","description":"When the row was created (UTC)."},"updated_at":{"type":"string","format":"date-time","description":"When the row last changed (UTC)."}},"required":["id","program_id","title","date","status","position","warmup","notes","log_notes","started_at","completed_at","source","external_id","created_at","updated_at"],"description":"A training session: a plan (`planned`), a session in progress, or history (`completed`)."},"WorkoutDetail":{"allOf":[{"$ref":"#/components/schemas/Workout"},{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutItem"},"description":"By position."},"created_exercises":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]},"description":"Only in POST / PUT responses, and only when some item names matched no exercise: the exercises this request created. Check it: a near-miss spelling here means a split history."}},"required":["items"]}],"description":"A workout with its items and their sets."},"WorkoutSummary":{"allOf":[{"$ref":"#/components/schemas/Workout"},{"type":"object","properties":{"item_count":{"type":"integer"},"set_count":{"type":"integer","description":"Working (non-warm-up) sets."},"done_set_count":{"type":"integer"},"volume_kg":{"type":"number","description":"Σ weight × reps over done working sets, in kg."},"muscle_groups":{"type":"array","items":{"$ref":"#/components/schemas/MuscleGroup"},"description":"Primary groups of the items, most frequent first."},"exercise_names":{"type":"array","items":{"type":"string"},"description":"Item names in order."}},"required":["item_count","set_count","done_set_count","volume_kg","muscle_groups","exercise_names"]}],"description":"A list row: the workout without items, plus counts."},"WorkoutList":{"type":"object","properties":{"workouts":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSummary"}},"total":{"type":"integer","description":"Matches before limit/offset."},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["workouts","total","limit","offset"],"description":"GET /api/workouts."},"LastPerformance":{"type":"object","properties":{"workout_id":{"type":"string"},"workout_title":{"type":"string"},"date":{"type":["string","null"],"format":"date"},"item_id":{"type":"string"},"prescription":{"type":["string","null"],"description":"The prescription at the time."},"result_text":{"type":["string","null"]},"sets":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSet"},"description":"Done sets only; empty for a text-only result."}},"required":["workout_id","workout_title","date","item_id","prescription","result_text","sets"],"description":"The most recent completed session of an exercise before this workout."},"SuggestedSet":{"type":"object","properties":{"weight":{"type":["number","null"]},"unit":{"$ref":"#/components/schemas/Unit"},"reps":{"type":["integer","null"]},"seconds":{"type":["integer","null"]}},"required":["weight","unit","reps","seconds"]},"Suggestion":{"type":"object","properties":{"sets":{"type":"array","items":{"$ref":"#/components/schemas/SuggestedSet"}},"reason":{"type":"string","description":"Why, e.g. `Hit 10 reps on every set on 24 Aug → +2.5 kg`, or for per-set targets `Hit every rep target (12·10·10) on 24 Aug → +2.5 kg`."},"kind":{"type":"string","enum":["progress","repeat","deload","new"],"description":"`progress` = heavier, or more reps at the same weight; `repeat` = same as last time (also: history logged only as text); `deload` = lighter, because the rep target is well above what he did at that weight; `new` = no history."}},"required":["sets","reason","kind"],"description":"Prefill for the next session of an exercise (see the suggestion rule)."},"NextResponse":{"type":"object","properties":{"workout":{"anyOf":[{"$ref":"#/components/schemas/WorkoutDetail"},{"type":"null"}],"description":"The in-progress workout, else the first planned one, with prefill. null when nothing is planned."},"upcoming":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutSummary"},"description":"The other planned workouts in queue order (at most 10)."}},"required":["workout","upcoming"]},"Stats":{"type":"object","properties":{"total_workouts":{"type":"integer","description":"Completed workouts with a date."},"total_sets":{"type":"integer","description":"Done working sets."},"total_volume_kg":{"type":"integer","description":"Rounded to 1 kg."},"this_week":{"type":"object","properties":{"workouts":{"type":"integer"},"sets":{"type":"integer"},"volume_kg":{"type":"integer"}},"required":["workouts","sets","volume_kg"],"description":"The ISO week (Monday start) that contains `today`."},"streak_weeks":{"type":"integer","description":"Consecutive weeks with a completed workout, ending this week (or last week when this week has none yet)."},"weekly":{"type":"array","items":{"type":"object","properties":{"week_start":{"type":"string","format":"date","description":"Monday."},"workouts":{"type":"integer"},"sets":{"type":"integer"},"volume_kg":{"type":"integer"}},"required":["week_start","workouts","sets","volume_kg"]},"description":"The last 12 weeks, oldest first."},"muscle_recency":{"type":"object","description":"One entry per MuscleGroup. `last_date` counts primary and secondary muscles (a squat trains the glutes too); `sets_7d` counts sets for the primary muscle only.","propertyNames":{"$ref":"#/components/schemas/MuscleGroup"},"additionalProperties":{"type":"object","properties":{"last_date":{"type":["string","null"],"format":"date"},"days_ago":{"type":["integer","null"],"description":"Days before `today`."},"sets_7d":{"type":"integer","description":"Done working sets in the 7 days ending `today`."}},"required":["last_date","days_ago","sets_7d"]}},"recent_prs":{"type":"array","items":{"type":"object","properties":{"exercise_id":{"type":"string"},"exercise_name":{"type":"string"},"kind":{"type":"string","enum":["heaviest","e1rm"]},"value":{"type":"number","description":"The weight as entered (heaviest) or the e1RM in kg."},"unit":{"$ref":"#/components/schemas/Unit"},"date":{"type":"string","format":"date"}},"required":["exercise_id","exercise_name","kind","value","unit","date"]},"description":"Records set in the 30 days before `today`, newest first, at most 10: a session's heaviest set or best e1RM that beats every earlier workout (a first session is not a record; an e1RM record is listed only when the session did not also set a heaviest record)."}},"required":["total_workouts","total_sets","total_volume_kg","this_week","streak_weeks","weekly","muscle_recency","recent_prs"],"description":"GET /api/stats. Counts cover completed workouts and done working sets."},"ExportData":{"type":"object","properties":{"version":{"type":"integer","const":1},"exported_at":{"type":"string","format":"date-time"},"exercises":{"type":"array","items":{"$ref":"#/components/schemas/Exercise"}},"programs":{"type":"array","items":{"$ref":"#/components/schemas/Program"}},"workouts":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutDetail"},"description":"Every workout with items and sets (no prefill fields), by date."}},"required":["version","exported_at","exercises","programs","workouts"],"description":"GET /api/export: every table."},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Machine-readable code (see Errors)."},"message":{"type":"string","description":"What went wrong, e.g. `items[2] needs exercise_id, exercise or name`."},"details":{"description":"Optional extra data."}},"required":["code","message"]}},"required":["error"],"description":"Every 4xx/5xx response."},"Deleted":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"id":{"type":"string"}},"required":["deleted","id"]},"Health":{"type":"object","properties":{"ok":{"type":"boolean"},"workouts":{"type":"integer","description":"Workouts stored."},"time":{"type":"string","format":"date-time","description":"Server time."}},"required":["ok","workouts","time"]},"ApiIndex":{"type":"object","description":"GET /api: resources, endpoints and links to the docs."},"SetInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: `s_` + random."},"position":{"type":"integer","minimum":0,"maximum":1000,"description":"Order within the item. Default: array order (or after the last set)."},"weight":{"type":["number","null"],"minimum":0,"maximum":2000,"description":"In `unit`, as entered. null = bodyweight or not recorded."},"unit":{"$ref":"#/components/schemas/Unit","description":"Default `kg` for a new set."},"reps":{"type":["integer","null"],"minimum":0,"maximum":1000},"seconds":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"For time-tracked exercises."},"rir":{"type":["number","null"],"minimum":0,"maximum":20,"description":"Reps in reserve he reported."},"is_warmup":{"type":"boolean","description":"Default false."},"done":{"type":"boolean","description":"Default false, except for a new set on a workout saved as `completed` (a session recorded afterwards), where it defaults to true. true stamps `done_at`; false clears it."},"done_at":{"type":["string","null"],"format":"date-time","description":"When it was logged. Default: now when `done` turns true. Only kept while `done` is true (import passes it through)."},"notes":{"type":["string","null"],"maxLength":2000},"updated_at":{"type":"string","format":"date-time","description":"Optional precondition (PUT): the set's `updated_at` as you read it. If the set changed since, the PUT is refused with 409 `would_lose_logged_sets` (unless `force=1`). A GET response carries it, so a GET → edit → PUT can't overwrite a set he logged in between."}},"description":"A set to create or update. Omitted fields keep their current value (or the default for a new set)."},"ItemInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: `i_` + random. On PUT, the id of an existing item of this workout updates that item; an item without an id takes the id of a current item with the same label and exercise, if any."},"exercise_id":{"type":["string","null"],"description":"Existing exercise id. Wins over `exercise` (except on an edited GET response: when `exercise_id` is the item's current exercise and `exercise` names another one, the name is the change). Unknown id: 422 `unknown_exercise`."},"exercise":{"type":"string","maxLength":200,"description":"Exercise **name**, matched against every exercise's name, then its aliases, then loosely (case, spaces, hyphens, apostrophes and a plural s ignored: `Strict Pull Up` = `Strict Pull-Up`), then by slug = exercise id. No match creates the exercise (listed in the response's `created_exercises`), with muscle group, equipment, tracking and per-side inferred from the words in the name."},"position":{"type":"integer","minimum":0,"maximum":1000,"description":"Default: array order in a workout body, or after the last item for POST …/items."},"label":{"type":["string","null"],"maxLength":5000,"description":"Superset label: `A1`, `A2`, `A3`, `B1` … or a bare letter for a single block."},"name":{"type":"string","maxLength":200,"description":"Display name. Default: the exercise's canonical name. Required for a free-form block (no exercise). A new item with only `name` that is exactly an exercise's name or alias links to that exercise; send `exercise_id: null` to keep it free-form."},"prescription":{"type":["string","null"],"maxLength":5000,"description":"Coach-style plan text, shown verbatim. Targets and cue are parsed from it; target fields sent alongside override the parsed values, except ones that only repeat the item's stored values when the text changed (an edited GET response). Clearing it (null or \"\") clears the targets and cue parsed from it."},"target_sets":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Parsed from `prescription` unless sent."},"target_reps":{"type":["array","null"],"items":{"type":["integer","null"],"minimum":0,"maximum":1000},"maxItems":50,"description":"Per-set targets, e.g. [12, 10, 8]; null = max reps."},"rep_min":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Bottom of the rep range."},"rep_max":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Top of the rep range."},"target_seconds":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Hold time per set."},"rest_seconds":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Rest between sets or rounds."},"rir":{"type":["integer","null"],"minimum":0,"maximum":86400,"description":"Target reps in reserve."},"cue":{"type":["string","null"],"maxLength":5000,"description":"Technique cue. Parsed from the prescription's extra lines unless sent."},"status":{"$ref":"#/components/schemas/ItemStatus","description":"Default `pending`."},"result_text":{"type":["string","null"],"maxLength":5000,"description":"Free-text result (text blocks, AMRAP scores)."},"notes":{"type":["string","null"],"maxLength":5000},"external_id":{"type":["string","null"],"maxLength":200,"description":"Optional key from another system."},"sets":{"type":"array","items":{"$ref":"#/components/schemas/SetInput"},"maxItems":50,"description":"The item's **complete** set list: listed sets (by id) are updated, sets without an id are created, sets left out are deleted. Omit to keep the current sets. A set id used twice in one body is a 400; one that belongs to another item is a 409. Normally left out of plans: sets are created from the suggestion when the workout starts (plan sets are only for a load you prescribe on purpose)."}},"description":"An item to create or update. Reference the exercise with `exercise` (name) or `exercise_id`; send only `name` for a free-form block. Omitted fields keep their current value."},"WorkoutInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: `w_` + random."},"program_id":{"type":["string","null"],"description":"Existing program id (else 422 `unknown_program`)."},"title":{"type":"string","maxLength":200,"description":"Required when creating, e.g. `Chest + Biceps + Core`."},"date":{"type":["string","null"],"format":"date","description":"`YYYY-MM-DD`. null = unscheduled (queued after every dated plan)."},"status":{"$ref":"#/components/schemas/WorkoutStatus","description":"Default `planned`. Prefer the start / complete / skip actions for transitions."},"position":{"type":"integer","minimum":-1000000,"maximum":1000000,"description":"Tie-breaker among planned workouts on the same date; lower first. Default 0."},"warmup":{"type":["string","null"],"maxLength":20000,"description":"Warm-up routine text."},"notes":{"type":["string","null"],"maxLength":20000,"description":"Plan notes shown before he starts (e.g. `Week 1/4. Heavier than last week.`)."},"log_notes":{"type":["string","null"],"maxLength":20000,"description":"His notes on how it went. Normally written by the app."},"source":{"type":"string","enum":["app","api","coachrx"],"description":"Default `api`."},"external_id":{"type":["string","null"],"maxLength":200,"description":"Optional unique key from another system. Create rejects a duplicate; import matches on it when the id is unknown."},"started_at":{"type":["string","null"],"format":"date-time","description":"Default: set by the status (`in_progress` → now). Send it only to restore history (import)."},"completed_at":{"type":["string","null"],"format":"date-time","description":"Default: set by the status (`completed` → now). Send it only to restore history (import)."},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemInput"},"maxItems":100,"description":"The **complete** item list: listed items (by id) are updated; an item without an id updates a current item with the same label and exercise (so re-sending a plan keeps its item ids), else it is created; items left out are deleted with their sets. Omit to keep the current items. On PUT, a change that would delete or undo logged work is a 409 unless `force=1`."}},"description":"A workout with optional nested items and sets. Omitted fields keep their current value (PUT) or take the default (create)."},"WorkoutCreate":{"allOf":[{"$ref":"#/components/schemas/WorkoutInput"},{"required":["title"]}],"description":"Body of POST /api/workouts: a WorkoutInput with a title."},"WorkoutPatch":{"type":"object","properties":{"program_id":{"type":["string","null"],"description":"Existing program id (else 422 `unknown_program`)."},"title":{"type":"string","maxLength":200,"description":"Required when creating, e.g. `Chest + Biceps + Core`."},"date":{"type":["string","null"],"format":"date","description":"`YYYY-MM-DD`. null = unscheduled (queued after every dated plan)."},"status":{"$ref":"#/components/schemas/WorkoutStatus","description":"Default `planned`. Prefer the start / complete / skip actions for transitions."},"position":{"type":"integer","minimum":-1000000,"maximum":1000000,"description":"Tie-breaker among planned workouts on the same date; lower first. Default 0."},"warmup":{"type":["string","null"],"maxLength":20000,"description":"Warm-up routine text."},"notes":{"type":["string","null"],"maxLength":20000,"description":"Plan notes shown before he starts (e.g. `Week 1/4. Heavier than last week.`)."},"log_notes":{"type":["string","null"],"maxLength":20000,"description":"His notes on how it went. Normally written by the app."},"source":{"type":"string","enum":["app","api","coachrx"],"description":"Default `api`."},"external_id":{"type":["string","null"],"maxLength":200,"description":"Optional unique key from another system. Create rejects a duplicate; import matches on it when the id is unknown."},"started_at":{"type":["string","null"],"format":"date-time","description":"Default: set by the status (`in_progress` → now). Send it only to restore history (import)."},"completed_at":{"type":["string","null"],"format":"date-time","description":"Default: set by the status (`completed` → now). Send it only to restore history (import)."}},"description":"Workout fields only; `items` is rejected (use PUT or the item endpoints)."},"ExerciseInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: slug of the name."},"name":{"type":"string","maxLength":200,"description":"Unique, case-insensitive, and not another exercise's alias (409). Renaming keeps the old name as an alias (unless `aliases` is sent), so items that still use it link here."},"aliases":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":50,"description":"Other names that resolve to this exercise when an item references it by name. Trimmed and de-duplicated; at most 50. A name or alias of another exercise is a 409."},"muscle_group":{"anyOf":[{"$ref":"#/components/schemas/MuscleGroup"},{"type":"null"}]},"secondary_muscles":{"type":"array","items":{"$ref":"#/components/schemas/MuscleGroup"}},"equipment":{"anyOf":[{"$ref":"#/components/schemas/Equipment"},{"type":"null"}]},"tracking":{"$ref":"#/components/schemas/Tracking","description":"Default `weight_reps`."},"per_side":{"type":"boolean","description":"Default false."},"default_unit":{"$ref":"#/components/schemas/Unit","description":"Default `kg`."},"weight_step":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Progression increment in `default_unit`. null = 2.5 kg / 5 lb."},"notes":{"type":["string","null"],"maxLength":5000}},"required":["name"],"description":"A new exercise."},"ExercisePatch":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"Unique, case-insensitive, and not another exercise's alias (409). Renaming keeps the old name as an alias (unless `aliases` is sent), so items that still use it link here."},"aliases":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":50,"description":"Other names that resolve to this exercise when an item references it by name. Trimmed and de-duplicated; at most 50. A name or alias of another exercise is a 409."},"muscle_group":{"anyOf":[{"$ref":"#/components/schemas/MuscleGroup"},{"type":"null"}]},"secondary_muscles":{"type":"array","items":{"$ref":"#/components/schemas/MuscleGroup"}},"equipment":{"anyOf":[{"$ref":"#/components/schemas/Equipment"},{"type":"null"}]},"tracking":{"$ref":"#/components/schemas/Tracking","description":"Default `weight_reps`."},"per_side":{"type":"boolean","description":"Default false."},"default_unit":{"$ref":"#/components/schemas/Unit","description":"Default `kg`."},"weight_step":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Progression increment in `default_unit`. null = 2.5 kg / 5 lb."},"notes":{"type":["string","null"],"maxLength":5000}},"description":"Exercise fields to change."},"ProgramInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: `p_` + random."},"name":{"type":"string","maxLength":200},"description":{"type":["string","null"],"maxLength":20000},"status":{"type":"string","enum":["active","archived"],"description":"Default `active`."}},"required":["name"],"description":"A new program."},"ProgramPatch":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":["string","null"],"maxLength":20000},"status":{"type":"string","enum":["active","archived"],"description":"Default `active`."}},"description":"Program fields to change."},"StartInput":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"The day it is done. Default: `today`."},"sets":{"type":"object","description":"Sets to create per item id instead of the suggestion (the app sends these after an offline start). Only applies to items without sets; items not listed get the suggestion. A set id used twice is a 400.","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/SetInput"}}},"items":{"type":"object","description":"Optional, per item id in `sets`: what the phone knew about the item. When that id no longer exists (the plan was rewritten after the phone cached it), its sets go to the item for the same exercise that has no sets, or the item is re-created, so offline logging is not lost.","additionalProperties":{"type":"object","properties":{"exercise_id":{"type":["string","null"]},"label":{"type":["string","null"]},"name":{"type":["string","null"]}},"required":["exercise_id","label","name"]}}}},"CompleteInput":{"type":"object","properties":{"discard_undone":{"type":"boolean","description":"Default true: delete sets never marked done. false keeps them (still not done). A JSON boolean (a string is a 400)."},"log_notes":{"type":["string","null"],"description":"How it went. Replaces `log_notes` when sent; null or \"\" clears it."},"date":{"type":"string","format":"date","description":"The day it was done. Default: the workout's date if it was in progress or already completed, else `today`."}}},"DuplicateInput":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Client-generated id (optional), matching `^[A-Za-z0-9_-]{1,64}$`. Default: `w_` + random."},"title":{"type":"string","maxLength":200,"description":"Default: the source title."},"date":{"type":["string","null"],"format":"date","description":"Default: null (unscheduled)."},"source":{"type":"string","enum":["app","api","coachrx"],"description":"Default `api`. The app sends `app` for copies made on the phone."}}},"ReorderInput":{"type":"object","properties":{"item_ids":{"type":"array","items":{"type":"string"},"description":"Every item id of the workout exactly once, in the new order."}},"required":["item_ids"]},"MergeInput":{"type":"object","properties":{"into":{"type":"string","description":"Id of the exercise to keep."}},"required":["into"]},"ImportInput":{"type":"object","properties":{"exercises":{"type":"array","items":{"$ref":"#/components/schemas/ExerciseInput"},"description":"Upserted by id (default: slug of the name)."},"programs":{"type":"array","items":{"$ref":"#/components/schemas/ProgramInput"},"description":"Upserted by id (no id: a new program each time)."},"workouts":{"type":"array","items":{"$ref":"#/components/schemas/WorkoutInput"},"description":"Upserted like PUT: by id, else by external_id, else created. Export rows are accepted as they are."}},"description":"Any subset of the export shape. Processed in order (exercises, programs, workouts), one row at a time: a failing row stops the import, earlier rows stay saved."},"ImportResult":{"type":"object","properties":{"imported":{"type":"object","properties":{"exercises":{"type":"integer"},"programs":{"type":"integer"},"workouts":{"type":"integer"}},"required":["exercises","programs","workouts"]}},"required":["imported"]},"ParseInput":{"type":"object","properties":{"prescription":{"type":"string","description":"Prescription text to parse."},"result":{"type":"string","description":"A free-text result to parse into sets, e.g. `25kg x 12 x 3`."}}},"ParsedPrescription":{"type":"object","properties":{"target_sets":{"type":["integer","null"]},"target_reps":{"type":["array","null"],"items":{"type":["integer","null"]},"description":"Only for per-set schemes (12-10-8) or max-rep sets (nulls)."},"rep_min":{"type":["integer","null"]},"rep_max":{"type":["integer","null"]},"target_seconds":{"type":["integer","null"]},"rest_seconds":{"type":["integer","null"]},"rir":{"type":["integer","null"]},"cue":{"type":["string","null"]},"per_side":{"type":"boolean","description":"`/side`, `per leg`, `each arm` …"},"kind":{"type":"string","enum":["sets","freeform"],"description":"`freeform` for AMRAP / EMOM / circuits: no set or rep targets."}},"required":["target_sets","target_reps","rep_min","rep_max","target_seconds","rest_seconds","rir","cue","per_side","kind"],"description":"What the API stores when this text is sent as `prescription`."},"ParsedSet":{"type":"object","properties":{"weight":{"type":["number","null"]},"unit":{"$ref":"#/components/schemas/Unit"},"reps":{"type":["integer","null"]},"seconds":{"type":["integer","null"]},"is_warmup":{"type":"boolean"}},"required":["weight","unit","reps","seconds","is_warmup"]},"ParsedResult":{"type":"object","properties":{"status":{"type":"string","enum":["done","skipped","empty"]},"sets":{"type":"array","items":{"$ref":"#/components/schemas/ParsedSet"}},"note":{"type":["string","null"],"description":"Text that isn't set data."},"confidence":{"type":"string","enum":["high","low"],"description":"`low` when the parser had to guess."}},"required":["status","sets","note","confidence"]},"ParseOutput":{"type":"object","properties":{"prescription":{"anyOf":[{"$ref":"#/components/schemas/ParsedPrescription"},{"type":"null"}]},"result":{"anyOf":[{"$ref":"#/components/schemas/ParsedResult"},{"type":"null"}]}},"required":["prescription","result"]}},"parameters":{"today":{"name":"today","in":"query","required":false,"description":"Your local day (YYYY-MM-DD). Drives the long-break check in suggestions, the stats windows and default dates. Default: the server's UTC date.","schema":{"type":"string","format":"date"},"example":"2026-09-24"}},"responses":{"BadRequest":{"description":"Invalid input (`bad_request`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Unknown id (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"workout 'w_nope' not found"}}}}},"Conflict":{"description":"Already exists or belongs elsewhere (`conflict`, `already_completed`, `in_progress`, `would_lose_logged_sets`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unprocessable":{"description":"A reference does not exist (`unknown_exercise`, `unknown_program`, `invalid_reference`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}