# Kinetic plan-file guide

## Short version
Return one UTF-8 JSON object with format "kinetic-plan" and schemaVersion 1.
Keep stable UUIDs for the plan, each entry and each exercise when updating.
Dates are local YYYY-MM-DD; durations are seconds, distances metres, weights pounds.
Use strength/cardio/mindBody/hiit payloads; rest has none.
Unknown activities are skipped; invalid known payloads reject the file.
Structured intervals use step nodes and one level of repeat blocks.
Guided sessions are 10/20/30/45 minutes; timers are 5–90 in steps of 5.
Files are at most 1 MiB, 20 plans, 400 entries and 50 exercises per entry.
Check the file at https://kineticpacer.com/community/validator.

## 1 What this file is
A .kinetic-plan file gives Kinetic a training plan across activities.
A coach can send it as a file and the client reviews it before importing.
Sending the same stable plan id again updates upcoming entries while preserving past work.

## 2 Quick rules
- JSON only, with format "kinetic-plan" and schemaVersion 1.
- Local dates use YYYY-MM-DD, never timestamps.
- Canonical units are meters, seconds and pounds; 0 pounds means bodyweight.
- Invent UUIDs once for each plan, entry and exercise, and keep them on updates.
- Several entries may share a date. No week/block label field is added.

## Plan ID and planCode
Each plan has a short Plan ID the app shows, for example E3A6-4EEF: the first 8 hex characters of `plan.id`, upper-case, split 4-4.
Through the MCP connector, `get_training_plan` returns it as `planCode` beside each plan. When the person names a plan by its Plan ID, match it to `planCode`; if it matches the start of more than one `planCode`, ask the user which plan they mean.
Plan ID is only for finding the plan: an update must send the full `plan.id`, never the short Plan ID.
If two plans share the 8 characters, their `planCode` values are lengthened (E3A6-4EEF-1) until they differ.
You may only update a plan you created through this connection; any other plan arrives as a new plan.

## 3 Fields
| Field | Type | Req | Rule |
|---|---|---|---|
| `format` | string | yes | Exactly `"kinetic-plan"`. Lets #1175 auto-detect. Missing/other → *invalid* (legacy strength files are detected by their own shape, §P.7). |
| `schemaVersion` | integer | yes | `1`. `> 1` → *newer-version* outcome. `< 1`, non-integer → *invalid*. |
| `exportedAt` | string | yes | ISO-8601 UTC instant with `Z`, e.g. `2026-09-27T14:05:00Z`. Fractional seconds allowed. |
| `appVersion` | string | yes | 1–40 chars. App: `"1.0.12 (57)"` (`CFBundleShortVersionString (CFBundleVersion)` / Android `versionName (versionCode)`); web builder: `"web-builder/<semver>"`; AI via MCP: `"mcp/<semver>"`. Informational only. |
| `scope` | object | no | Present only when the export covers part of a plan (Share Plan "This week" / "Next 4 weeks"). `{"start":"YYYY-MM-DD","endExclusive":"YYYY-MM-DD"}`, start < endExclusive, span ≤ 400 days. Absent = whole plan. Semantics in §U. |
| `plans` | array | yes | 1–20 plan objects. Empty → *invalid*. |

Unknown top-level keys: ignored (forward compatibility). This rule applies at every level: unknown keys are ignored, never an error.

**§P.2 Plan**
| Field | Type | Req | Rule |
|---|---|---|---|
| `id` | string (UUID) | yes | RFC-4122 textual UUID, any case on read; writers emit lowercase. Update-by-id key (D3). |
| `name` | string | yes | 1–100 chars after trimming whitespace. |
| `goal` | string | no | 0–500 chars. Default `""`. |
| `notes` | string | no | 0–500 chars. Default `""`. |
| `coachName` | string | no | 0–60 chars, plain text (Q5). Empty string ≡ absent. Displayed as "From <coachName>". |
| `clientFirstName` | string | no | 0–40 chars. Echoed into week files (§W). |
| `entries` | array | yes | 0–400 entries (the 400 cap is **per file, summed across plans**). |

**§P.3 Entry**
| Field | Type | Req | Rule |
|---|---|---|---|
| `id` | string (UUID) | yes | Unique within the file (duplicate → *invalid*). |
| `date` | string | yes | Local calendar date `YYYY-MM-DD` (Gregorian, proleptic). Never an instant. Invalid date (e.g. `2026-02-30`) → *invalid*. |
| `activity` | string | yes | See §P.4. |
| `title` | string | yes | 1–100 chars after trim. |
| `notes` | string | no | 0–500 chars. |
| exactly one payload key | object | see §P.5 | `strength` / `cardio` / `mindBody` / `hiit`; `rest` and skipped activities have **no** payload key. A known activity with the wrong/missing payload key, or two payload keys → *invalid*. |

**§P.4 `activity` values**
| Value | Family / payload key |
|---|---|
| `strength` | `strength` |
| `running`, `walking`, `cycling`, `stairMaster`, `rowing` | `cardio` |
| `yoga`, `pilates`, `stretching` | `mindBody` |
| `hiit` | `hiit` |
| `rest` | none |
| `breathing`, `customWorkout` | **skipped** with reason (known to Kinetic, not plannable in v1.0.12) |
| any other string | **skipped** with reason |

Decode activity as a plain string before selecting a payload. Skipped entries still validate their id, date, activity, title and optional notes; their payloads are ignored. Unknown activities use `"<title>": <activity-name> isn't in Kinetic yet.` where activity-name splits camel case and lowercases the raw value. Breathing uses `"<title>": Breathing can't be planned yet.`; Custom uses `"<title>": Custom workouts can't be planned yet.` (owner D-28).


**Payloads**


`strength` (object): `exercises` array, 1–50 items. Each item is **exactly** the current transfer record:
| Field | Type | Rule |
|---|---|---|
| `id` | UUID | unique in the file |
| `exerciseIdentifier` | string | 1–120 chars; library id (unknown ids are accepted as custom exercises, same as today's strength import) |
| `exerciseName` | string | 1–100 chars |
| `exerciseCategory` | string | one of `push pull squat hinge lunge carry rotation isolation core`; unknown → `isolation` on import (warning `W_CATEGORY_DEFAULTED`, #1175) |
| `equipment` | string | one of `bodyweight dumbbell barbell kettlebell cable machine band smith trap_bar`; unknown → `bodyweight` + warning |
| `bodyRegionsRaw` | string | comma-joined `StrengthBodyRegion` raw values (`chest,upperBack,…`), 0–15 items; unknown tokens dropped (same as `StrengthPersistenceCodec.decode`) |
| `sortOrder` | integer | ≥ 0; ties broken by array order |
| `targetSets` | integer | 1–8 (today's planner editor range) |
| `targetReps` | integer | 1–100 |
| `targetWeight` | number | **pounds**, finite, 0–1500 (0 = bodyweight). Stored exactly as received; never rounded on store. |
| `targetRestSeconds` | integer | 0–600 in files; the editor offers 30–240 in 15 s steps; values outside the editor range are kept and shown as-is (the stepper snaps only when the user edits). |

`cardio` (object):
| Field | Type | Rule |
|---|---|---|
| `target` | string | `time` \| `distance` \| `open` \| `intervals` |
| `durationSeconds` | integer | required iff `target=time`; 60–43,200 (1 min–12 h) |
| `distanceMeters` | number | required iff `target=distance`; 100–300,000 |
| `zone` | integer 1–5 or string `"any"` | required unless `target=intervals` (then forbidden; zones live on steps) |
| `steps` | array | required iff `target=intervals`; §P.I |
Extra target fields that don't belong to the chosen `target` → *invalid* (e.g. `durationSeconds` with `target=open`).

`mindBody` (object):
| Field | Type | Rule |
|---|---|---|
| `mode` | string | `guided` \| `timer` |
| `focus` | string | required iff `mode=guided`; must be a focus id for the entry's activity from `mindbody-pack.json` — yoga `full_flow hips_legs back_core balance`; pilates `full_body core legs_glutes wall`; stretching `full_body upper lower neck_shoulders mobility`. Wrong id → *invalid*. Forbidden when `mode=timer`. |
| `durationMinutes` | integer | guided: exactly 10, 20, 30 or 45 (D5), anything else → *invalid*; timer: 5–90, in 5-minute steps (owner D-20). |

`hiit` (object): `rounds`, `workSeconds`, `restSeconds` (integers). Rounds 1–20; work and rest 10–120 seconds (owner D-21). Unplanned HIIT settings are unchanged.


**Interval nodes**
 (`cardio.steps`)
A node is a **step** or a **repeat**.
- Step: `{"type":"step","kind":"warmup|work|recovery|cooldown","measure":"time|distance","seconds":int?,"meters":number?,"zone":1..5|"any"?,"recoveryKind":"walk|jog"?}`
  - `seconds` required iff `measure=time`: 5–7,200. `meters` required iff `measure=distance`: 25–42,195.
  - `zone` optional (absent = no zone target, rendered without a zone). `recoveryKind` allowed only when `kind=recovery`; optional.
- Repeat: `{"type":"repeat","count":int,"steps":[step,…]}` — `count` 1–99; children are **steps only** (one nesting level; a repeat inside a repeat → *invalid*); 1–10 children.
- `steps` top level: 1–50 nodes. Fully expanded step count ≤ 1,000.
- Totals sum time-step seconds and distance-step metres independently, multiplying repeat children by count. No pace is assumed. Story: **29 min timed · 2.4 km** (owner D-15).



## 4 Limits
Plan raw bytes ≤1,048,576 (1 MiB); 1–20 plans; ≤400 entries summed across plans; 1–50 exercises per strength entry; ≤500 Unicode scalar values per string unless a smaller field cap is stated; JSON container nesting depth ≤32. Duplicate UUIDs anywhere across plans, entries and exercises are invalid (UUID case does not distinguish identities). Duplicate decoded JSON keys are rejected with E_DUPLICATE_KEY before generic parsing. All numbers must be finite. Integral JSON numbers such as 8.0 are valid integers; 8.5, booleans and numeric strings are not. Unknown keys are ignored at every level. Week raw bytes ≤5,242,880 (5 MiB); reports ≤16,384 compressed bytes and ≤65,536 decompressed bytes. Scope span ≤400 days; intervals ≤50 top-level nodes, repeat count 1–99, 1–10 step children, ≤1,000 fully expanded steps. No nested repeat. Enforce limits before any write.

## 5 A complete example week
```json
{
  "format": "kinetic-plan",
  "schemaVersion": 1,
  "exportedAt": "2026-09-27T14:05:00Z",
  "appVersion": "1.0.12 (57)",
  "plans": [
    {
      "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000001",
      "name": "5K Build",
      "goal": "5K under 25:00",
      "notes": "Three runs, two lifts, one mobility day. Week 3 of 8.",
      "entries": [
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000128",
          "date": "2026-09-28",
          "activity": "running",
          "title": "Easy run",
          "notes": "Keep it conversational. Nose-breathing if you can.",
          "cardio": {
            "target": "time",
            "durationSeconds": 1800,
            "zone": 2
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000129",
          "date": "2026-09-29",
          "activity": "strength",
          "title": "Upper body A",
          "notes": "",
          "strength": {
            "exercises": [
              {
                "id": "00000000-0000-4000-8000-000000002711",
                "exerciseIdentifier": "Barbell_Bench_Press_-_Medium_Grip",
                "exerciseName": "Barbell Bench Press - Medium Grip",
                "exerciseCategory": "push",
                "equipment": "barbell",
                "bodyRegionsRaw": "chest",
                "sortOrder": 0,
                "targetSets": 4,
                "targetReps": 8,
                "targetWeight": 135,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002712",
                "exerciseIdentifier": "Seated_Cable_Rows",
                "exerciseName": "Seated Cable Rows",
                "exerciseCategory": "pull",
                "equipment": "cable",
                "bodyRegionsRaw": "chest",
                "sortOrder": 1,
                "targetSets": 4,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002713",
                "exerciseIdentifier": "Dumbbell_Shoulder_Press",
                "exerciseName": "Dumbbell Shoulder Press",
                "exerciseCategory": "push",
                "equipment": "dumbbell",
                "bodyRegionsRaw": "chest",
                "sortOrder": 2,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 40,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002714",
                "exerciseIdentifier": "Wide-Grip_Lat_Pulldown",
                "exerciseName": "Wide-Grip Lat Pulldown",
                "exerciseCategory": "pull",
                "equipment": "cable",
                "bodyRegionsRaw": "chest",
                "sortOrder": 3,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002715",
                "exerciseIdentifier": "Plank",
                "exerciseName": "Plank",
                "exerciseCategory": "core",
                "equipment": "bodyweight",
                "bodyRegionsRaw": "chest",
                "sortOrder": 4,
                "targetSets": 3,
                "targetReps": 45,
                "targetWeight": 0,
                "targetRestSeconds": 90
              }
            ]
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000130",
          "date": "2026-09-30",
          "activity": "pilates",
          "title": "Pilates core",
          "notes": "",
          "mindBody": {
            "mode": "guided",
            "focus": "core",
            "durationMinutes": 30
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000101",
          "date": "2026-10-01",
          "activity": "running",
          "title": "Intervals",
          "notes": "400 m hard, 90 s walk between.",
          "cardio": {
            "target": "intervals",
            "steps": [
              {
                "type": "step",
                "kind": "warmup",
                "measure": "time",
                "seconds": 600,
                "zone": 2
              },
              {
                "type": "repeat",
                "count": 6,
                "steps": [
                  {
                    "type": "step",
                    "kind": "work",
                    "measure": "distance",
                    "meters": 400,
                    "zone": 4
                  },
                  {
                    "type": "step",
                    "kind": "recovery",
                    "measure": "time",
                    "seconds": 90,
                    "recoveryKind": "walk"
                  }
                ]
              },
              {
                "type": "step",
                "kind": "cooldown",
                "measure": "time",
                "seconds": 600,
                "zone": 1
              }
            ]
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000102",
          "date": "2026-10-02",
          "activity": "strength",
          "title": "Lower body B",
          "notes": "",
          "strength": {
            "exercises": [
              {
                "id": "00000000-0000-4000-8000-000000002716",
                "exerciseIdentifier": "Barbell_Squat",
                "exerciseName": "Barbell Squat",
                "exerciseCategory": "squat",
                "equipment": "barbell",
                "bodyRegionsRaw": "chest",
                "sortOrder": 0,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002717",
                "exerciseIdentifier": "Romanian_Deadlift",
                "exerciseName": "Romanian Deadlift",
                "exerciseCategory": "hinge",
                "equipment": "barbell",
                "bodyRegionsRaw": "chest",
                "sortOrder": 1,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002718",
                "exerciseIdentifier": "Leg_Press",
                "exerciseName": "Leg Press",
                "exerciseCategory": "squat",
                "equipment": "machine",
                "bodyRegionsRaw": "chest",
                "sortOrder": 2,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-000000002719",
                "exerciseIdentifier": "Standing_Calf_Raises",
                "exerciseName": "Standing Calf Raises",
                "exerciseCategory": "isolation",
                "equipment": "machine",
                "bodyRegionsRaw": "chest",
                "sortOrder": 3,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 90,
                "targetRestSeconds": 90
              },
              {
                "id": "00000000-0000-4000-8000-00000000271a",
                "exerciseIdentifier": "Plank",
                "exerciseName": "Plank",
                "exerciseCategory": "core",
                "equipment": "bodyweight",
                "bodyRegionsRaw": "chest",
                "sortOrder": 4,
                "targetSets": 3,
                "targetReps": 10,
                "targetWeight": 0,
                "targetRestSeconds": 90
              }
            ]
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000103",
          "date": "2026-10-03",
          "activity": "stretching",
          "title": "Mobility",
          "notes": "",
          "mindBody": {
            "mode": "timer",
            "durationMinutes": 20
          }
        },
        {
          "id": "0b6f6c1e-5c8b-4a9e-9d3a-000000000104",
          "date": "2026-10-04",
          "activity": "rest",
          "title": "Rest",
          "notes": "Walk if you feel like it"
        }
      ],
      "coachName": "Coach Sola",
      "clientFirstName": "Alex"
    }
  ]
}
```

## 6 Prompt for your AI
You are writing a training plan file for the Kinetic app. Follow the Kinetic plan-file guide below exactly. Output only valid JSON, no explanations, no code fences. Use format "kinetic-plan" and schemaVersion 1. Invent new UUIDs for the plan, every day and every exercise, and reuse the same plan id if you are updating a plan you wrote before. Dates are YYYY-MM-DD. Distances are meters, times are seconds, strength weights are pounds (0 means bodyweight). Here is what I want: <describe the week>.

## 7 Check your file
https://kineticpacer.com/community/validator

This guide is served verbatim by get_plan_file_guide and exported as kinetic-plan-file-guide.md by the later app integration.
