# Tempisto file formats

Tempisto uses two JSON documents:

- Session (`.session.json`) — one workout.
- Plan (`.plan.json`) — a schedule of sessions.

All durations are ISO 8601 duration strings of the form
`P[nD][T[nH][nM][nS]]`, for example `PT15M`, `PT2M30S`, `PT10S`. Years and
months are not allowed. A duration is always written as a string, never as a
number of seconds.

## Session file (`.session.json`)

Top-level object:

- `format` (string, required): must be `tempisto.session/1`.
- `name` (string, required): display name of the session.
- `sport` (string, optional): default `running`.
- `notes` (string, optional): free-form notes.
- `steps` (array, required): list of step objects or repeat blocks. At least
  one step.
- `announcements` (object, optional): voice settings and announcement rules.
- `modifiedEpochMillis` (number, optional): last edit time in epoch
  milliseconds. Written by the editors; hand-written files may omit it.
- `archived` (boolean, optional): archived sessions stay on disk and stay
  referenced by plans, they only leave the pickers.

### Step object

- `type` (string, required): one of `warmup`, `work`, `recovery`, `cooldown`,
  `rest`.
- `name` (string, optional): custom name of the step.
- `duration` (object, required): exactly one of the following forms.
  - `{ "time": "<ISO duration>" }` — a fixed time.
  - `{ "distance": { "value": <number>, "unit": "m" | "km" | "mi" } }` — a
    fixed distance.
  - `{ "until": { "metric": <metric>, "below" | "above": <number>, "for":
"<ISO duration>" }, "maxTime" | "maxDistance": ... }` — ends when a
    condition holds, capped by `maxTime` (an ISO duration) or `maxDistance` (a
    distance object). The cap is required.
    - `until` metrics: `power`, `pace`, `speed`, `heart_rate`, `cadence`.
    - Use exactly one of `below` or `above`. `for` is optional and is the time
      the condition must hold.
  - `{ "lap": true }` — ends when the lap control is pressed.
- `target` (object, optional): one metric as the key, for example
  `{ "power": { "relative": [0.65, 0.75] } }`. Metrics: `power`, `pace`,
  `speed`, `heart_rate`, `cadence`.
- `announce` (object, optional): `{ "rules": [ ... ] }` with the same rule
  shape as `announcements.rules`.

### Repeat block

- `{ "repeat": <number>, "steps": [ ... ] }`
- `repeat` is the number of repetitions (1 or more).
- Blocks nest: the `steps` of a repeat block may hold step objects or further
  repeat blocks.

### Announcements object

- `voice` (object):
  - `enabled` (boolean)
  - `rate` (number, 0.35 to 0.75), where 0.5 is the system default
  - `duckOtherAudio` (boolean)
  - `voiceIdentifier` (string, optional)
  - `languageCode` (string, optional), a BCP-47 tag
- `haptics` (boolean)
- `rules` (array): each rule carries
  - a trigger, exactly one of
    - `{ "every": { "distance": { "value": <number>, "unit": "m" | "km" |
"mi" } } }`
    - `{ "every": { "time": "<ISO duration>" } }`
    - `{ "on": "step_start" | "step_end" | "target_violation" }`
    - `{ "on": "last_seconds", "at": "<ISO duration>" }`
  - `say` (array of strings, required): the announcement fields. The list must
    not be empty.
  - `minInterval` (ISO duration, optional): minimum time between two firings of
    the rule.

### Announcement fields (`say`)

Composed fields follow the grammar `[<scope>_][<aggregate>_]<metric>`:

- scope: `total`, `step`, `split`, `lap`
- aggregate: `avg`, `max`, `min`, `remaining`
- metric: `time`, `distance`, `pace`, `speed`, `power`, `cadence`,
  `heart_rate`, `elevation`

Standalone fields: `step_name`, `step_target`, `target_deviation`, `countdown`,
`distance_source`, `step_duration`, `next_step`, `go`, `average_speed`.

Examples of valid fields: `step_name`, `step_target`, `countdown`,
`step_avg_power`, `step_time`, `split_avg_pace`, `total_distance`,
`total_avg_power`, `total_time`, `target_deviation`.

## Plan file (`.plan.json`)

Top-level object:

- `format` (string, required): must be `tempisto.plan/1`.
- `name` (string, required).
- `starts` (string, required): plan start date as `YYYY-MM-DD`.
- `entries` (array, required): each entry carries
  - `session` (string, required): file name of a session file.
  - `rrule` (string) or `date` (string, `YYYY-MM-DD`) — exactly one of the two.
  - `rrule` is an iCal recurrence rule, for example
    `FREQ=WEEKLY;BYDAY=MO,WE;COUNT=16`.

## Examples

`5x1000.session.json`:

```json
{
  "format": "tempisto.session/1",
  "name": "5 × 1000 m @ CP",
  "sport": "running",
  "notes": "Keep the recoveries honest.",
  "steps": [
    {
      "type": "warmup",
      "duration": { "time": "PT15M" },
      "target": { "power": { "relative": [0.65, 0.75] } }
    },
    {
      "repeat": 5,
      "steps": [
        {
          "type": "work",
          "name": "1000 m",
          "duration": { "distance": { "value": 1000, "unit": "m" } },
          "target": { "power": { "relative": [0.98, 1.02] } },
          "announce": {
            "rules": [
              { "on": "step_start", "say": ["step_name", "step_target"] },
              { "on": "last_seconds", "at": "PT10S", "say": ["countdown"] },
              { "on": "step_end", "say": ["step_avg_power", "step_time"] }
            ]
          }
        },
        {
          "type": "recovery",
          "duration": {
            "until": { "metric": "heart_rate", "below": 130, "for": "PT10S" },
            "maxTime": "PT3M"
          }
        }
      ]
    },
    { "type": "cooldown", "duration": { "time": "PT10M" } }
  ],
  "announcements": {
    "voice": { "enabled": true, "rate": 0.5, "duckOtherAudio": true },
    "haptics": true,
    "rules": [
      {
        "every": { "distance": { "value": 500, "unit": "m" } },
        "say": ["split_avg_pace", "total_distance"]
      },
      { "every": { "time": "PT5M" }, "say": ["total_avg_power", "total_time"] },
      {
        "on": "target_violation",
        "say": ["target_deviation"],
        "minInterval": "PT20S"
      }
    ]
  }
}
```

`base-build.plan.json`:

```json
{
  "format": "tempisto.plan/1",
  "name": "Base build, 8 weeks",
  "starts": "2026-08-10",
  "entries": [
    {
      "session": "easy-45.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=MO,WE;COUNT=16"
    },
    {
      "session": "5x1000.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=TU;COUNT=8"
    },
    {
      "session": "long-run.session.json",
      "rrule": "FREQ=WEEKLY;BYDAY=SU;COUNT=8"
    },
    { "session": "benchmark.session.json", "date": "2026-10-04" }
  ]
}
```

## Rules

- Durations are strings in ISO 8601 form; years and months are not allowed.
- A plan entry has `rrule` or `date`, never both and never neither.
- A rule that says nothing has no effect: `say` must not be empty.
- `target_deviation` only reads correctly inside a `target_violation` rule.
- A timed step and a distance step must be longer than zero.
- An `until` step without `maxTime` or `maxDistance` is not valid.
- A `repeat` count must be at least 1.
