---
title: "Health Connect exercise routes: ExerciseRouteResult and the per-session consent flow"
canonical: "https://aifitnessapi.com/health-connect-api/exercise-route-consent"
cluster: "Health Connect API"
primary_query: "ExerciseRouteResult.ConsentRequired"
last_reviewed: "2026-10-04"
description: "Why Health Connect returns ExerciseRouteResult.ConsentRequired, how ExerciseRouteRequestContract asks for one route, and the two route permissions."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"Health Connect exercise routes: ExerciseRouteResult and the per-session consent flow\", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/exercise-route-consent"
---

# Health Connect exercise routes: ExerciseRouteResult and the per-session consent flow

> A Health Connect exercise route is read from ExerciseSessionRecord.exerciseRouteResult, which is one of three results: Data with the route, NoData when the session has none, or ConsentRequired, which Google's reference describes as "a permission hasn't been granted and a value couldn't be returned." READ_EXERCISE_ROUTES cannot be requested through the normal permission contract; the user grants it in Settings, or approves one route at a time through the dialog launched by ExerciseRouteRequestContract with the session ID. Google says routes created by other apps cannot be read while your app is in the background.

- Canonical: https://aifitnessapi.com/health-connect-api/exercise-route-consent
- Last reviewed: 2026-10-04
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "Health Connect exercise routes: ExerciseRouteResult and the per-session consent flow", AIFitnessAPI, https://aifitnessapi.com/health-connect-api/exercise-route-consent

---

GPS routes are the one part of a Health Connect exercise session that the ordinary read permission does not unlock. Read a session written by another app and its route may come back as "ask the user first" rather than as data. This page covers `ExerciseRoute`, the three `ExerciseRouteResult` cases, and the per-session consent flow, according to Google's exercise-routes guide and the Jetpack reference.

## The data: ExerciseRoute and its locations

`ExerciseRoute`, added in 1.1.0, "Captures a route associated with an exercise session a user does." The reference adds that it "Contains a sequence of location points, with timestamps, which do not have to be in order." Its one property is `route: List<ExerciseRoute.Location>`.

| `Location` field | Type | Reference |
| --- | --- | --- |
| `time` | `Instant` | "The point in time when the location was recorded; Required field." |
| `latitude` | `Double` | "Required field; Valid range -90; 90" |
| `longitude` | `Double` | "Required field; Valid range -180; 180" |
| `horizontalAccuracy` | `Length?` | Optional; "Valid range: non-negative numbers." |
| `verticalAccuracy` | `Length?` | Optional; "Valid range: non-negative numbers." |
| `altitude` | `Length?` | Optional. |

A route has no record of its own. It lives on `ExerciseSessionRecord`: you write it through the constructor's `exerciseRoute` parameter and read it back through the session's `exerciseRouteResult` property. The reference on that property: "Location data points of `ExerciseRoute` should be within the parent session, and should be before the end time of the session." The session record itself is described on the [ExerciseSessionRecord page](/health-connect/exercise-session-record).

## The three results

`exerciseRouteResult` is an `ExerciseRouteResult`, "Result of the route associated with an exercise session a user does." It is always one of three subclasses, all added in 1.1.0:

| Subclass | Reference | What to do |
| --- | --- | --- |
| `ExerciseRouteResult.Data` | "Class containing data for an `ExerciseRoute`." | Read `exerciseRoute.route`. |
| `ExerciseRouteResult.ConsentRequired` | "Class indicating that a permission hasn't been granted and a value couldn't be returned." | Ask the user, with the consent contract below. |
| `ExerciseRouteResult.NoData` | "Class indicating that there's no data to request permissions for." | Nothing; the session has no route. |

`ConsentRequired` means a route exists. Do not show "no route recorded" for it.

## The two permissions, and why one cannot be requested

| Permission | Jetpack constant | Purpose |
| --- | --- | --- |
| `android.permission.health.READ_EXERCISE_ROUTES` | `HealthPermission.PERMISSION_READ_EXERCISE_ROUTES` | Read routes written by other apps without asking each time. |
| `android.permission.health.WRITE_EXERCISE_ROUTE` | `HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE` | Write a route as part of a session. |

Google's guide flags the spelling: "For this permission type, `READ_EXERCISE_ROUTES` is plural, while `WRITE_EXERCISE_ROUTE` is singular." Both sit on top of the exercise permissions, because "each route is associated with an exercise session (one session = one workout)."

The read permission is the unusual one. The Jetpack reference: "This permission can't be granted via the standard permission request mechanism, and can only be granted by a user in Settings, or via the dialog launched by `androidx.health.connect.client.contracts.ExerciseRouteRequestContract`." The framework reference, where it was added in API level 35 and U Extensions 12, is blunter: "Attempts to request the permission by applications will be ignored." Putting it in your `createRequestPermissionResultContract` set does nothing.

Google lists the access paths like this: "For the session owner, data is accessed using a session read." and "From a third-party app, through a dialog that allows the user to grant a one-time read of a route." So the app that wrote a session gets its route back from a normal session read. For another app's route, the reference says that with the read permission granted "the app can read exercise routes without user interaction", and that when it is revoked "exercise routes can only be obtained by launching the `androidx.health.connect.client.contracts.ExerciseRouteRequestContract` intent".

## The per-session consent flow

`ExerciseRouteRequestContract` is "An `ActivityResultContract` to request a route associated with an `androidx.health.connect.client.records.ExerciseSessionRecord`. It receives the session id as input and returns an ExerciseRoute as output, if available." The input is the session's `metadata.id`; `createIntent` throws `IllegalArgumentException` "if the `input` is an empty string"; and `parseResult` returns `null` "if the user didn't grant access to the exercise route or if there's no exercise route for the session id passed on `createIntent`."

This follows Google's reference sample:

```kotlin
val requestRoute = registerForActivityResult(ExerciseRouteRequestContract()) { route: ExerciseRoute? ->
    if (route != null) {
        showRoute(route)            // your code
    } else {
        // consent denied, or no route for this session
    }
}

suspend fun openSessionMap(client: HealthConnectClient, sessionId: String) {
    val session = client.readRecord(ExerciseSessionRecord::class, sessionId).record
    when (val result = session.exerciseRouteResult) {
        is ExerciseRouteResult.Data -> showRoute(result.exerciseRoute)
        is ExerciseRouteResult.ConsentRequired -> requestRoute.launch(sessionId)
        is ExerciseRouteResult.NoData -> Unit
        else -> Unit
    }
}
```

Google's samples keep an `else` branch: the reference declares `ExerciseRouteResult` as an `abstract class`, so Kotlin does not treat the three subclasses as exhaustive. `readRecord` takes the session's `metadata.id` and, like the [readRecords](/health-connect-api/readrecords-pagination) family generally, needs the exercise read permission.

## Background reads do not include other apps' routes

Google's guide: "Your app can't read exercise route data created by other apps when it runs in the background. When your app runs in the background and tries to read an exercise route created by another app, Health Connect returns an `ExerciseRouteResult.ConsentRequired` response, even if your app has Always allow access to exercise route data." Its recommendation: "we strongly recommend that you request routes upon deliberate user interaction with your app, when the user is actively engaged with your app's UI."

The Jetpack reference for the read permission says reading apps "must be in the foreground unless `android.permission.health.READ_HEALTH_DATA_IN_BACKGROUND` is also granted", which sounds more permissive. The guide's sentence is specific to routes created by other apps, so plan for those to need the foreground whatever else is granted. [Background reads](/health-connect-api/read-health-data-in-background) covers the rest.

Google's UI guidance gives the matching advice for when to ask: "Ask for exercise routes permission after users finish a workout in your app." More on that is on the [permission UI guidelines page](/health-connect-api/permission-ui-guidelines).

## Writing a route

Pass the route in the session constructor, and only when the write permission is granted. The reference: "An attempt to insert/update a session with a set route without the permission granted will result in a failed call and the session insertion/update will be rejected."

```kotlin
val granted = client.permissionController.getGrantedPermissions()
val route = if (HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE in granted) {
    ExerciseRoute(points.map { p ->      // points: your recorded fixes
        ExerciseRoute.Location(
            time = p.time,
            latitude = p.lat,
            longitude = p.lon,
            horizontalAccuracy = Length.meters(p.accuracyMeters)
        )
    })
} else null

client.insertRecords(listOf(
    ExerciseSessionRecord(
        startTime = start, startZoneOffset = ZoneOffset.UTC,
        endTime = end, endZoneOffset = ZoneOffset.UTC,
        exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
        exerciseRoute = route,
        metadata = Metadata.activelyRecorded(device = Device(type = Device.TYPE_PHONE))
    )
))
```

Updating a session later has a trap. Google's guide: "If your app has a route write permission and tries to update a session by passing in a session object without a route, the existing route is deleted." The reference covers the other case: "If the permission is not granted the previously written route will not be deleted if the session gets updated with no route set."

## Pitfalls

- **Requesting `READ_EXERCISE_ROUTES` in the normal permission set.** It is ignored. Use `ExerciseRouteRequestContract` per session, or send the user to Health Connect settings.
- **Treating `ConsentRequired` as "no route".** It means a route exists and you lack access. Only `NoData` means there is none.
- **Reading routes from a worker.** Other apps' routes come back as `ConsentRequired` in the background. Fetch them when the user opens the workout.
- **Updates that silently drop the route.** With the write permission held, an update without `exerciseRoute` deletes the stored route. Carry the route through when you update a session.
- **A spelling mismatch in Google's own tables.** Google's data-types table lists `android.permission.health.READ_EXERCISE_ROUTE`; the framework constant's value and the routes guide use `READ_EXERCISE_ROUTES`. Use the plural.
- **Points outside the session.** The reference says points should fall within the session and before its end time. Trim stray GPS fixes before writing.
- **Looking for an aggregate.** Google lists only `EXERCISE_DURATION_TOTAL` as an aggregation for `ExerciseSessionRecord`, and its own session sample reads `DistanceRecord` over the session's time range when it wants distance. The HealthKit counterpart, which returns points in batches, is [HKWorkoutRouteQuery](/healthkit-queries/hkworkoutroutequery).

## FAQ

### Why does Health Connect return ExerciseRouteResult.ConsentRequired for a session that has a route?

Because your app lacks route access for that session. Google's reference says ConsentRequired indicates a permission has not been granted and a value could not be returned. It appears for routes written by other apps when the user has not granted route access, and, per Google's guide, whenever your app tries to read another app's route from the background, even with Always allow access.

[Permalink](https://aifitnessapi.com/health-connect-api/exercise-route-consent#faq-1)

### Can I request READ_EXERCISE_ROUTES with createRequestPermissionResultContract?

No. Google's Jetpack reference says the permission cannot be granted through the standard permission request mechanism and can only be granted by the user in Settings or through the dialog launched by ExerciseRouteRequestContract. The Android framework reference adds that attempts by applications to request it are ignored.

[Permalink](https://aifitnessapi.com/health-connect-api/exercise-route-consent#faq-2)

### How does ExerciseRouteRequestContract work?

It is an ActivityResultContract whose input is the exercise session's ID from its metadata and whose output is an ExerciseRoute or null. Launching it shows a dialog that lets the user grant a one-time read of that session's route. Google's reference says the result is null if the user did not grant access or if the session has no route, and that an empty session ID throws IllegalArgumentException.

[Permalink](https://aifitnessapi.com/health-connect-api/exercise-route-consent#faq-3)

### Why did updating a Health Connect exercise session delete its route?

Because your app holds the route write permission and sent the session without a route. Google's exercise-routes guide says that if an app with route write permission updates a session by passing a session object without a route, the existing route is deleted. Without that permission, the reference says a previously written route is kept when the session is updated with no route set.

[Permalink](https://aifitnessapi.com/health-connect-api/exercise-route-consent#faq-4)
