---
title: "PlannedExerciseSessionRecord: training plans in Health Connect"
canonical: "https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans"
cluster: "Health Connect API"
primary_query: "PlannedExerciseSessionRecord"
last_reviewed: "2026-10-04"
description: "How Health Connect training plans work per Google's docs: PlannedExerciseSessionRecord, blocks and steps, goals and targets, and linking the session."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"PlannedExerciseSessionRecord: training plans in Health Connect\", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans"
---

# PlannedExerciseSessionRecord: training plans in Health Connect

> PlannedExerciseSessionRecord is Health Connect's training-plan record, which Google's reference describes as "a planned exercise session, also commonly referred to as a training plan." A plan holds blocks, each block repeats a list of steps, and each step has an exercise phase, a completion goal and performance targets such as a heart-rate range; its start and end may be in the future. The workout app links its ExerciseSessionRecord back by setting plannedExerciseSessionId, and Health Connect fills in the plan's completedExerciseSessionId automatically. The record requires FEATURE_PLANNED_EXERCISE and has no supported aggregations.

- Canonical: https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans
- Last reviewed: 2026-10-04
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "PlannedExerciseSessionRecord: training plans in Health Connect", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans

---

Health Connect can carry a workout before it happens. A coaching app writes the plan, a watch or workout app reads it and records the session, and the coaching app reads the result back to see whether the targets were hit. The record type behind this is `PlannedExerciseSessionRecord`. This page covers its structure, the permissions each side needs, and how a finished session links back to its plan, according to Google's training-plans guide and the Jetpack reference.

## What Google says it is

The reference, where the class is listed as added in 1.1.0: "Captures a planned exercise session, also commonly referred to as a training plan. Each record contains a start time, end time, an exercise type and a list of [PlannedExerciseBlock] which describe the details of the planned session. The start and end times may be in the future."

It also says: "Requires `androidx.health.connect.client.HealthConnectFeatures.FEATURE_PLANNED_EXERCISE`." Google's guide opens with the check:

```kotlin
val plansSupported = healthConnectClient.features.getFeatureStatus(
    HealthConnectFeatures.FEATURE_PLANNED_EXERCISE
) == HealthConnectFeatures.FEATURE_STATUS_AVAILABLE
```

The flag is the constant `3`, added in 1.1.0; the general pattern is on the [getFeatureStatus page](/health-connect-api/getfeaturestatus-feature-availability). Google's guide states "There are no supported aggregations for this data type."

## The structure: session, blocks, steps

| Level | Class | Fields (reference) |
| --- | --- | --- |
| Session | `PlannedExerciseSessionRecord` | `startTime`, `endTime` and zone offsets, `exerciseType`, `blocks`, `title`, `notes`, `metadata`, `completedExerciseSessionId`, `hasExplicitTime` |
| Block | `PlannedExerciseBlock` | `repetitions: Int`, `steps: List<PlannedExerciseStep>`, `description: String?` |
| Step | `PlannedExerciseStep` | `exerciseType`, `exercisePhase`, `completionGoal`, `performanceTargets`, `description` |

A block is "a series of `PlannedExerciseStep`s"; Google's guide explains that blocks exist "to support repetition of different groups of steps (for example, do a sequence of arm curls, burpies, and crunches five times in a row)." A step is "A single step within an `PlannedExerciseBlock` e.g. 8x 60kg barbell squats."

Each step has a phase, one of `EXERCISE_PHASE_UNKNOWN` (0), `EXERCISE_PHASE_WARMUP` (1), `EXERCISE_PHASE_REST` (2), `EXERCISE_PHASE_ACTIVE` (3), `EXERCISE_PHASE_COOLDOWN` (4) and `EXERCISE_PHASE_RECOVERY` (5), which the reference calls "Lower intensity, active exercise."

A step's `completionGoal` is "The goal that must be completed to finish this step", an `ExerciseCompletionGoal` subclass: `ActiveCaloriesBurnedGoal`, `DistanceAndDurationGoal`, `DistanceGoal`, `DurationGoal`, `ManualCompletion`, `RepetitionsGoal`, `StepsGoal`, `TotalCaloriesBurnedGoal` or `UnknownGoal`.

Its `performanceTargets` are "Performance related targets that should be met during this step", each an `ExercisePerformanceTarget`: `AmrapTarget`, `CadenceTarget`, `HeartRateTarget`, `PowerTarget`, `RateOfPerceivedExertionTarget`, `SpeedTarget`, `UnknownTarget` or `WeightTarget`. For cadence the reference notes "The value may be interpreted as RPM for e.g. cycling activities, or as steps per minute for e.g. walking/running activities."

## Two ways to set the time

The record has two constructors. One takes `startTime`, `startZoneOffset`, `endTime` and `endZoneOffset` like other interval records; for the offsets, "If null is provided, this will default to the current system timezone." The other takes a `startDate: LocalDate` and a `duration: Duration`, for plans that name a day but no time: "The start time will be implicitly generated from a local time at noon in the system default timezone on the day specified by `startDate`." The reference also lists a `hasExplicitTime` property, with no description.

## Writing a plan

This follows Google's training-plans example: a run with three laps and a heart-rate target between 90 and 110 bpm.

```kotlin
val plan = PlannedExerciseSessionRecord(
    startTime = start,
    startZoneOffset = null,
    endTime = start.plus(Duration.ofMinutes(90)),
    endZoneOffset = null,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    blocks = listOf(
        PlannedExerciseBlock(
            repetitions = 1,
            steps = listOf(
                PlannedExerciseStep(
                    exerciseType = ExerciseSegment.EXERCISE_SEGMENT_TYPE_RUNNING,
                    exercisePhase = PlannedExerciseStep.EXERCISE_PHASE_ACTIVE,
                    completionGoal = ExerciseCompletionGoal.RepetitionsGoal(repetitions = 3),
                    performanceTargets = listOf(
                        ExercisePerformanceTarget.HeartRateTarget(minHeartRate = 90.0, maxHeartRate = 110.0)
                    )
                )
            ),
            description = "Three laps around the lake"
        )
    ),
    title = "Run at lake",
    notes = null,
    metadata = Metadata.activelyRecorded(device = Device(type = Device.TYPE_PHONE))
)
val planId = healthConnectClient.insertRecords(listOf(plan)).recordIdsList.firstOrNull()
```

Note the two kinds of exercise type: the session takes an `ExerciseSessionRecord` constant, while Google's sample fills the step's `exerciseType` with an `ExerciseSegment` constant. Every record needs factory-made metadata, explained on the [Metadata page](/health-connect-api/metadata-recording-method), and the returned ID is what the workout app will point back to.

## Linking the finished session to the plan

The workout app records the session as an ordinary `ExerciseSessionRecord` and sets `plannedExerciseSessionId` to the plan's ID. In Google's example, "the ID would be determined by the user selecting a planned exercise session from a list of existing sessions."

```kotlin
val session = ExerciseSessionRecord(
    startTime = actualStart, startZoneOffset = ZoneOffset.UTC,
    endTime = actualEnd, endZoneOffset = ZoneOffset.UTC,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    title = "Run at lake",
    plannedExerciseSessionId = planId,
    metadata = Metadata.activelyRecorded(device = Device(type = Device.TYPE_PHONE))
)
healthConnectClient.insertRecords(listOf(session))
```

The link back is automatic. Google's guide lists, among the fields of a planned session, "Completed exercise session ID — This is written automatically after an exercise session related to this planned exercise session is completed." On the record that is `completedExerciseSessionId`, "The exercise session that completed this planned session." So the plan app can go from a session to its plan through `plannedExerciseSessionId`, or from a plan to its session through `completedExerciseSessionId`.

## Evaluating targets

Google's last step reads recent sessions, follows `plannedExerciseSessionId` to the plan with `readRecord`, and aggregates heart rate over the session's time range. A corrected version of that check:

```kotlin
suspend fun heartRateRange(client: HealthConnectClient, exercise: ExerciseSessionRecord) {
    val planId = exercise.plannedExerciseSessionId ?: return
    val planned = client.readRecord(PlannedExerciseSessionRecord::class, planId).record

    val hr = client.aggregate(
        AggregateRequest(
            metrics = setOf(HeartRateRecord.BPM_MIN, HeartRateRecord.BPM_MAX),
            timeRangeFilter = TimeRangeFilter.between(exercise.startTime, exercise.endTime)
        )
    )
    val minBpm = hr[HeartRateRecord.BPM_MIN] ?: return
    val maxBpm = hr[HeartRateRecord.BPM_MAX] ?: return
    // your code: compare with planned.blocks -> steps -> performanceTargets
}
```

Then walk `planned.blocks`, each block's `steps`, and each step's `performanceTargets`, comparing a `HeartRateTarget`'s `minHeartRate` and `maxHeartRate` with the measured range. Aggregates are covered on the [aggregate page](/health-connect-api/aggregate-request).

## Who needs which permission

Planned exercise has its own pair, `android.permission.health.READ_PLANNED_EXERCISE` and `android.permission.health.WRITE_PLANNED_EXERCISE`, requested as `HealthPermission.getReadPermission(PlannedExerciseSessionRecord::class)` and `getWritePermission(...)`. Google notes that "often the app that creates training plans and evaluates performance against plans isn't the same as the app that consumes training plans and writes actual exercise data", and splits its heart-rate example like this:

| Training plan app | Workout app |
| --- | --- |
| `WRITE_PLANNED_EXERCISE` | `READ_PLANNED_EXERCISE` |
| `READ_EXERCISE` | `WRITE_EXERCISE` |
| `READ_EXERCISE_ROUTES` | `WRITE_EXERCISE_ROUTE` |
| `READ_HEART_RATE` | `WRITE_HEART_RATE` |

Google's key point on the same table: "Write permission also provides you read access to records you have written using that permission. It isn't always necessary to request both read and write permission for a data type." The route rows follow the consent rules on the [exercise route page](/health-connect-api/exercise-route-consent).

## Pitfalls

- **Google's evaluation sample reads metrics it never requested.** It builds the `AggregateRequest` with only `HeartRateRecord.BPM_AVG`, then reads `BPM_MAX` and `BPM_MIN`. `AggregationResult.get` is documented to return "the value of the metric, or null if not set", so request every metric you read, as in the sketch above.
- **Skipping the feature check.** The record "Requires" `FEATURE_PLANNED_EXERCISE`. Hide plan screens when it is unavailable.
- **Null zone offsets mean the device's current zone.** The constructor docs say a null offset defaults to the current system timezone, so a plan written while the phone is set to another zone picks up that zone. Pass explicit offsets when you know the zone the user plans in.
- **A date-only plan lands at noon.** The `LocalDate` constructor places the start at noon in the system default timezone. Show such plans as "on this day", not "at 12:00".
- **No aggregates on plans.** Google lists none. Totals come from the performed session's data types, not from the plan.
- **Trying to write the completed ID yourself.** `completedExerciseSessionId` is not a constructor parameter, and Google says it is "written automatically". Set `plannedExerciseSessionId` on the session instead.

The data type itself, with its permissions and fields, is listed on the [PlannedExerciseSessionRecord data page](/health-connect/planned-exercise-session-record).

## FAQ

### How do I link a completed workout to a Health Connect training plan?

Set plannedExerciseSessionId on the ExerciseSessionRecord you write to the ID of the PlannedExerciseSessionRecord, which insertRecords returns when the plan is written. Google's training-plans guide says the plan's completed exercise session ID is then written automatically, and the reference exposes it as completedExerciseSessionId on the planned record.

[Permalink](https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans#faq-1)

### What is the difference between a PlannedExerciseBlock and a PlannedExerciseStep?

A step is one exercise instruction, such as Google's example of eight 60 kg barbell squats, with a phase, a completion goal and optional performance targets. A block groups steps and repeats them a set number of times; Google's guide gives the example of a sequence of arm curls, burpees and crunches done five times in a row. A planned session holds a list of blocks.

[Permalink](https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans#faq-2)

### Which permissions does a training plan app need compared with a workout app in Health Connect?

Google's guide splits them by role. In its heart-rate example, the app that writes plans and evaluates them needs WRITE_PLANNED_EXERCISE plus read access to exercise, exercise routes and heart rate, while the workout app needs READ_PLANNED_EXERCISE plus write access to exercise, exercise routes and heart rate. Google adds that write permission also gives read access to records your app wrote.

[Permalink](https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans#faq-3)

### What time does a Health Connect training plan get when created with a LocalDate?

Noon on that day. Google's reference says the PlannedExerciseSessionRecord constructor that takes a startDate and a duration generates the start time from a local time at noon in the system default timezone, and the end time by adding the duration. Use the constructor with explicit start and end instants when the plan has a fixed time.

[Permalink](https://aifitnessapi.com/health-connect-api/planned-exercise-session-training-plans#faq-4)
