> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/planned-workouts-api/core-concepts.md).

# Core Concepts

### Workout Structure

A workout template is a hierarchy of blocks and steps:

```
WorkoutTemplate
├── name: "FTP Intervals"
├── sport: "cycling"
├── environment: "outdoor" (optional: indoor, outdoor, pool)
├── description: "Build threshold power"
├── pool_length_meters: null (only for swimming)
├── estimated_duration_seconds: 1800 (optional)
├── estimated_distance_meters: null (optional)
├── estimated_calories: null (optional)
├── estimated_tss: null (optional, 0-9999)
├── estimated_intensity_factor: null (optional, 0-5)
│
└── step_blocks: [
      StepBlock {
        completion_condition: { type: "reps", value: 4 }  ← Repeat 4 times
        steps: [
          WorkoutStep { ... },  ← Do this
          WorkoutStep { ... }   ← Then this
        ]
      }
    ]
```

### Step Blocks

A step block groups one or more steps that execute together. The block's `completion_condition` controls repetition:

| Block Completion                        | Behavior                      |
| --------------------------------------- | ----------------------------- |
| `{ "type": "reps", "value": 4 }`        | Repeat all steps 4 times      |
| `{ "type": "time", "value": 1200 }`     | Repeat until 20 minutes total |
| `{ "type": "distance", "value": 5000 }` | Repeat until 5km total        |
| `{ "type": "open" }` or omitted         | Execute once (no repeat)      |

#### Example: 4x400m Intervals

```json
{
  "step_blocks": [{
    "completion_condition": { "type": "reps", "value": 4 },
    "steps": [
      {
        "completion_condition": { "type": "distance", "value": 400 },
        "intensity_type": "active"
      },
      {
        "completion_condition": { "type": "distance", "value": 200 },
        "intensity_type": "rest"
      }
    ]
  }]
}
```

This creates: 400m fast → 200m easy → 400m fast → 200m easy → 400m fast → 200m easy → 400m fast → 200m easy

### Workout Steps

Each step defines:

| Field                  | Required | Description                                                      |
| ---------------------- | -------- | ---------------------------------------------------------------- |
| `completion_condition` | Yes      | When this step ends                                              |
| `intensity_type`       | Yes      | How hard (warmup/active/rest/recovery/cooldown)                  |
| `intensity_targets`    | No       | Target metrics (HR, power, pace, etc.)                           |
| `notes`                | No       | Text shown to athlete                                            |
| `controls`             | No       | Equipment settings held during the step: `grade` or `resistance` |
| `strength`             | No       | Exercise details for strength training                           |
| `swimming`             | No       | Stroke/equipment for swimming                                    |

### Completion Conditions

How a step (or block) ends:

| Type                 | Value   | Description                                |
| -------------------- | ------- | ------------------------------------------ |
| `time`               | Seconds | End after duration (e.g., 300 = 5 minutes) |
| `distance`           | Meters  | End after distance (e.g., 1000 = 1km)      |
| `reps`               | Count   | End after N repetitions                    |
| `calories`           | kcal    | End after calories burned                  |
| `open`               | —       | Manual end (lap button)                    |
| `hr_less_than`       | BPM     | End when HR drops below                    |
| `hr_greater_than`    | BPM     | End when HR rises above                    |
| `power_less_than`    | Watts   | End when power drops below                 |
| `power_greater_than` | Watts   | End when power rises above                 |

#### Example: Recovery Until HR Drops

```json
{
  "completion_condition": {
    "type": "hr_less_than",
    "value": 120
  },
  "intensity_type": "rest"
}
```

### Intensity Types

| Type       | Display         | Use For                       |
| ---------- | --------------- | ----------------------------- |
| `warmup`   | Warmup          | Gradual start                 |
| `active`   | Work / Interval | Main effort                   |
| `rest`     | Rest            | Full stop between intervals   |
| `recovery` | Recovery        | Easy effort between intervals |
| `cooldown` | Cooldown        | Gradual end                   |

### Intensity Targets

Targets define the goal metric for a step. You can have multiple targets per step (provider-dependent).

#### Target Types

**Absolute Targets:**

| Target Type  | Value Unit | Example                       | Provider Support                       |
| ------------ | ---------- | ----------------------------- | -------------------------------------- |
| `heart_rate` | BPM        | 150-160 BPM                   | All except Hevy                        |
| `power`      | Watts      | 250-280W                      | All except Huawei and Hevy             |
| `speed`      | m/s        | 4.0-4.5 m/s                   | All except Hevy                        |
| `pace`       | sec/km     | 270-300 sec/km (4:30-5:00/km) | All except Hevy                        |
| `cadence`    | rpm or spm | 170-180 spm                   | All except Hevy (Zepp: secondary only) |
| `rpe`        | 1-10       | 7-8 RPE                       | Wahoo, TrainingPeaks only              |

**Percentage Targets** (require athlete parameters at scheduling):

| Target Type                       | Requires               | Example                  |
| --------------------------------- | ---------------------- | ------------------------ |
| `heart_rate_max_percentage`       | `max_heart_rate`       | 80-85% of max HR         |
| `heart_rate_threshold_percentage` | `threshold_heart_rate` | 95-100% of threshold     |
| `power_percentage`                | `ftp`                  | 90-95% of FTP            |
| `speed_percentage`                | `threshold_speed`      | 85-90% of threshold pace |

**Zone Targets** (device-configured zones):

| Target Type       | Value | Provider Support                                                                                                            |
| ----------------- | ----- | --------------------------------------------------------------------------------------------------------------------------- |
| `heart_rate_zone` | 1-5   | All except Hevy (Garmin native, others convert to BPM or %). Uses threshold HR (COROS, Zepp) or max HR (others) as baseline |
| `power_zone`      | 1-7   | All except Huawei and Hevy (Garmin native, others convert to % FTP or watts)                                                |

#### Target Value Formats

Targets support single values or ranges. Percentage targets (`heart_rate_max_percentage`, `heart_rate_threshold_percentage`, `power_percentage`, `speed_percentage`) must use `value_low` and/or `value_high`: an exact `value` is rejected with a 400. Expanding a single value into a range also returns a coercion warning.

```json
// Range (recommended)
{
  "target_type": "power_percentage",
  "value_low": 90,
  "value_high": 95
}

// Single value (Terra expands to ±5% range automatically, except RPE which stays as a point value)
{
  "target_type": "heart_rate",
  "value": 155
}
```

### Sport Types

| Sport                | Description               | Provider Support                                  |
| -------------------- | ------------------------- | ------------------------------------------------- |
| `running`            | Running                   | All except Hevy                                   |
| `cycling`            | Cycling                   | All except Huawei and Hevy                        |
| `swimming`           | Swimming                  | Garmin, COROS, Suunto, TrainingPeaks, Zepp, Apple |
| `strength`           | Weight training           | Garmin, COROS, Suunto, TrainingPeaks, Apple, Hevy |
| `rowing`             | Rowing                    | TrainingPeaks, Apple                              |
| `yoga`               | Yoga                      | Garmin, Apple                                     |
| `pilates`            | Pilates                   | Garmin only                                       |
| `cardio`             | General cardio            | Garmin only                                       |
| `trail_running`      | Trail running             | Garmin, COROS, Wahoo, Suunto, Zepp                |
| `mountain_biking`    | Mountain biking           | Garmin, Wahoo, Suunto, TrainingPeaks              |
| `backcountry_skiing` | Backcountry / ski touring | Garmin, Suunto                                    |
| `hiking`             | Hiking                    | Garmin, Suunto                                    |
| `walking`            | Walking                   | Garmin, Suunto, TrainingPeaks                     |
| `elliptical`         | Elliptical                | Garmin                                            |
| `stair_climbing`     | Stair climbing            | None native, coerced with a warning               |

Unsupported sport types are coerced to the provider's closest equivalent with a coercion warning. See [Provider Compatibility](/planned-workouts-api/provider-compatibility.md) for details.

### Environment (Optional)

Use the `environment` field to distinguish indoor/outdoor variants:

| Environment | Description                               |
| ----------- | ----------------------------------------- |
| `indoor`    | Indoor (treadmill, stationary bike, etc.) |
| `outdoor`   | Outdoor                                   |
| `pool`      | Pool swimming                             |

If omitted, the provider uses its default. Examples:

```json
{ "sport": "running", "environment": "indoor" }    // Treadmill
{ "sport": "cycling", "environment": "indoor" }    // Stationary bike
{ "sport": "swimming", "environment": "pool" }     // Lap swimming
{ "sport": "swimming", "environment": "outdoor" }  // Open water swimming
{ "sport": "running" }                              // Default (outdoor)
```

### Workout Estimates (Optional)

Estimates describe the workout as a whole, not a single step. All five are optional.

| Field                        | Unit    | Notes                         |
| ---------------------------- | ------- | ----------------------------- |
| `estimated_duration_seconds` | Seconds | Planned total time            |
| `estimated_distance_meters`  | Meters  | Planned total distance        |
| `estimated_calories`         | kcal    | Planned energy                |
| `estimated_tss`              | 0-9999  | Planned Training Stress Score |
| `estimated_intensity_factor` | 0-5     | Planned Intensity Factor      |

Terra forwards all five to TrainingPeaks when the workout is scheduled or rescheduled, and returns them on workouts read back from any provider that exposes them.

TrainingPeaks derives TSS, Intensity Factor and duration from the athlete's own thresholds whenever any step target is not RPE. Your values are ignored in that case, and Terra returns coercion warnings on `workout.estimated_tss`, `workout.estimated_intensity_factor` and `workout.estimated_duration_seconds`. Distance and calories are always forwarded.

An `estimated_tss` outside 0-9999, or an `estimated_intensity_factor` outside 0-5, is rejected when the template is created.

### Complete Example

A 30-minute cycling workout with warmup, FTP intervals, and cooldown:

```json
{
  "name": "30min FTP Builder",
  "sport": "cycling",
  "description": "4x4min @ 95-100% FTP with 2min recovery",
  "step_blocks": [
    {
      "steps": [{
        "completion_condition": { "type": "time", "value": 300 },
        "intensity_type": "warmup",
        "intensity_targets": [{
          "target_type": "power_percentage",
          "value_low": 50,
          "value_high": 60
        }]
      }]
    },
    {
      "completion_condition": { "type": "reps", "value": 4 },
      "steps": [
        {
          "completion_condition": { "type": "time", "value": 240 },
          "intensity_type": "active",
          "intensity_targets": [{
            "target_type": "power_percentage",
            "value_low": 95,
            "value_high": 100
          }]
        },
        {
          "completion_condition": { "type": "time", "value": 120 },
          "intensity_type": "rest",
          "intensity_targets": [{
            "target_type": "power_percentage",
            "value_low": 40,
            "value_high": 50
          }]
        }
      ]
    },
    {
      "steps": [{
        "completion_condition": { "type": "time", "value": 300 },
        "intensity_type": "cooldown",
        "intensity_targets": [{
          "target_type": "power_percentage",
          "value_low": 40,
          "value_high": 50
        }]
      }]
    }
  ]
}
```

### Reading Workouts Back

`GET /plannedWorkouts?user_id=X` returns this same structure. Every item carries the workout body under `workout`: name, sport, `step_blocks` and estimates, whether you scheduled it through Terra or the athlete created it on the provider.

Provider-created workouts are marked `is_external: true`, have no `workout_id`, and are listed for Garmin, Wahoo, Suunto, TrainingPeaks and Intervals.icu. Their `sport` is `unspecified` when the provider's sport has no Terra equivalent. `warnings` lists anything that could not be represented exactly when the provider's workout was read into this structure. Repeat blocks are preserved.

Intervals.icu is read-only for planned workouts: workouts created there are listed, but Terra cannot push workouts to it.

Without `start_date` and `end_date`, provider-created workouts default to the trailing 30 days. Pass an explicit window to list upcoming workouts. When the provider cannot be read the response is still a 200 carrying the workouts Terra scheduled, with `X-Terra-Provider-Data-Truncated: true` and `X-Terra-Provider-Fetch` giving the reason.

### Validation Rules

Templates are validated when created. Key rules:

| Field                        | Requirement                                                                                                                        |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `name`                       | Required, non-empty                                                                                                                |
| `sport`                      | Required, must be a valid sport type                                                                                               |
| `step_blocks`                | At least one block required                                                                                                        |
| Each block                   | At least one step required                                                                                                         |
| Each step                    | `intensity_type` required (`warmup`, `active`, `rest`, `recovery`, `cooldown`)                                                     |
| Each step                    | `completion_condition` defaults to `open` if omitted                                                                               |
| `pool_length_meters`         | Only allowed for swimming workouts                                                                                                 |
| `value_low` / `value_high`   | Cannot use both `value` and `value_low`/`value_high` on the same target                                                            |
| Range targets                | `value_low` must be strictly less than `value_high`                                                                                |
| Athlete metrics              | All values must be positive if provided (`ftp`, `max_heart_rate`, `threshold_heart_rate`, `threshold_speed`, `pool_length_meters`) |
| `planned_date`               | Format: `YYYY-MM-DD`                                                                                                               |
| `estimated_tss`              | Between 0 and 9999 if provided                                                                                                     |
| `estimated_intensity_factor` | Between 0 and 5 if provided                                                                                                        |
| Percentage targets           | Must use `value_low` and/or `value_high`, not `value`                                                                              |

**Target value sanity checks.** A value outside these ranges is rejected with a 400 when the template is created:

| Target Type        | Valid Range |
| ------------------ | ----------- |
| Heart rate (BPM)   | 30–250      |
| Power (watts)      | 1–2000      |
| Speed (m/s)        | 0–15        |
| Pace (sec/km)      | 1–7200      |
| Cadence (rpm/spm)  | 1–300       |
| RPE                | 1–10        |
| Percentage targets | 0–200%      |
