---
title: "HKWorkoutRouteQuery: reading a workout's GPS route in batches"
canonical: "https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery"
cluster: "HealthKit Queries"
primary_query: "HKWorkoutRouteQuery"
last_reviewed: "2026-10-03"
description: "Reading a HealthKit workout route: find the HKWorkoutRoute with an anchored query, then read CLLocation batches with HKWorkoutRouteQuery until done."
publisher: "AIFitnessAPI — independent, not sponsored"
cite_as: "\"HKWorkoutRouteQuery: reading a workout's GPS route in batches\", AIFitnessAPI, https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery"
---

# HKWorkoutRouteQuery: reading a workout's GPS route in batches

> HKWorkoutRouteQuery reads the CLLocation points stored in an HKWorkoutRoute sample and delivers them in batches: its data handler can run several times, and only the call where done is true means you have every location. Finding the route sample is a separate, earlier step, and Apple recommends an HKAnchoredObjectQuery for HKSeriesType.workoutRoute() filtered with HKQuery.predicateForObjects(from:) for the workout, because route samples can arrive after the workout is saved and can be replaced later. Apple notes that the stored locations are accurate within 50 meters and may need smoothing before you draw them.

- Canonical: https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery
- Last reviewed: 2026-10-03
- Publisher: AIFitnessAPI (https://aifitnessapi.com) — independent, not sponsored
- Cite as: "HKWorkoutRouteQuery: reading a workout's GPS route in batches", AIFitnessAPI, https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery

---

Reading a run's route from HealthKit takes two queries, and each has its own trap. The first can come back empty even though the workout has a route; the second hands you the route in pieces. This page covers both, from Apple's "Reading route data" article and the class documentation.

## Two queries, not one

Apple's article names the two challenges up front: "Route samples aren't static" and "After you have the route sample, you still need to perform a second query to access the underlying location data."

1. Find the `HKWorkoutRoute` sample attached to the workout.
2. Run an `HKWorkoutRouteQuery` on that sample to read its locations.

`HKWorkoutRoute` is "A sample that contains a workout's route data." Its locations are "stored as an array of CLLocation objects", and "Because the route may contain a large number of location objects, use a HKWorkoutRouteQuery object to asynchronously read the location data from the HealthKit store in batches."

**Availability.** `HKWorkoutRoute` is listed from iOS 11.0, iPadOS 11.0, Mac Catalyst 13.0, macOS 13.0, visionOS 1.0 and watchOS 4.0. Apple's `HKWorkoutRouteQuery` class page lists iOS, iPadOS, Mac Catalyst, macOS, visionOS and watchOS without an introduction version. The date-interval initializer is listed from iOS 16.0, iPadOS 16.0, Mac Catalyst 16.0, macOS 13.0, visionOS 1.0 and watchOS 9.0.

## Step 1: find the route sample, and keep watching

Why not a plain sample query? Apple explains that apps add route data after the workout exists: "an app must save a workout before associating route data with it. This means there is a brief period when the workout exists in the HealthKit store, but it doesn't yet have a route sample associated with it." Routes also get replaced: an app "may generate and save the initial route sample on Apple Watch, but then perform additional smoothing on iPhone or a remote server", then update the sample "using a sync identifier, replacing the original sample with the new, updated version."

Hence Apple's conclusion: "a query that just returns the current route sample for a given workout may return an outdated copy of the route, or may not return anything at all. To guarantee that your app receives the most up-to-date route sample, use an anchored object query to both get the current route, and to track any additions or updates." The type is `HKSeriesType.workoutRoute()`, "a series type object for workout routes", and the filter is `HKQuery.predicateForObjects(from:)` with the workout, which matches "any objects that have been associated with the provided workout." How the anchored query works is covered on the [HKAnchoredObjectQuery page](/healthkit-queries/hkanchoredobjectquery).

## Step 2: read the locations in batches

`init(route:dataHandler:)` takes the route and a handler with four parameters. Apple documents the handler as "A block called each time the system returns a batch of location data. This block may be called one or more times." The parameters are the query, `routeData` (a batch of locations, "or nil if an error has occurred"), `done`, and an error. Of `done`, Apple says: "It is true if all the location data has been returned. If one or more additional batches of data are still pending, it is false."

Apple's article lists the sequence: create the query, run it with `execute(_:)`, receive "one or more batches of location data", and optionally "Call the HealthKit store's stop(_:) method to stop the query from receiving additional data."

## Both steps together

```swift
import HealthKit
import CoreLocation

func readRoute(for workout: HKWorkout, store: HKHealthStore) {
    let forThisWorkout = HKQuery.predicateForObjects(from: workout)

    let routeQuery = HKAnchoredObjectQuery(type: HKSeriesType.workoutRoute(),
                                           predicate: forThisWorkout,
                                           anchor: nil,
                                           limit: HKObjectQueryNoLimit) { _, samples, _, _, error in
        guard let route = (samples as? [HKWorkoutRoute])?.first else { return }  // none yet is normal
        readLocations(of: route, store: store)
    }
    routeQuery.updateHandler = { _, samples, _, _, error in
        guard let route = (samples as? [HKWorkoutRoute])?.first else { return }  // added or replaced later
        readLocations(of: route, store: store)
    }
    store.execute(routeQuery)       // long-running: stop(_:) it when the screen goes away
}

func readLocations(of route: HKWorkoutRoute, store: HKHealthStore) {
    var points: [CLLocation] = []
    let query = HKWorkoutRouteQuery(route: route) { _, batch, done, error in
        guard let batch else { return }   // nil only when error is set
        points.append(contentsOf: batch)
        if done {
            // every location has arrived; smooth and draw points here
        }
    }
    store.execute(query)
}
```

Because the handler can run more than once, collect into one place and act only when `done` is true. Apple's sample comments the same thing: "This block may be called multiple times."

## Reading only part of a route

`init(route:dateInterval:dataHandler:)` restricts the read to a time range, for example one lap or one split. Apple: "If the date interval doesn't overlap with the specified workout, this query returns an empty array. If the date interval only partially overlaps the specified workout, the query only returns location data from the overlapping time period."

With Swift concurrency, `HKWorkoutRouteQueryDescriptor` returns the locations as an `AsyncSequence` that ends on its own once every location has arrived; see the [async query descriptors page](/healthkit-queries/healthkit-async-query-descriptors).

## What a route stores, and how good it is

Apple says `HKWorkoutRoute` "saves a subset of CLLocation properties including" timestamp, coordinate, altitude, speed, course, horizontalAccuracy, verticalAccuracy, speedAccuracy and courseAccuracy. Apple calls it a subset and does not say whether that list is complete, so code against those nine fields and treat any other `CLLocation` property as possibly missing after the round trip. For altitude change from the barometer rather than from route locations, see [CMAltimeter's relative and absolute altitude streams](/phone-sensors/cmaltimeter).

On quality, Apple's note: "Locations from the HealthKit store are accurate within 50 meters, but they may need additional smoothing before you can use them. For example, to produce clean lines when it's plotting the route on a map."

## Pitfalls

- **Drawing the first batch as the whole route.** Wait for `done`. A route of thousands of points arrives in several batches, which is the whole reason the class exists.
- **Concluding a workout has no route.** Right after a workout is saved, its route may not exist yet. An anchored query with an update handler catches it when it lands; a one-off sample query does not.
- **Keeping the first copy forever.** Apps may replace a route with a smoothed version. The update handler is how you learn about it.
- **Plotting raw points.** Apple says the locations may need additional smoothing.
- **Sharing routes without thought.** A route can reveal where someone lives or works. [Building social features](/engagement/social-features-fitness-app) covers safer ways to show a route to other users, and [GPS activity APIs](/data/gps-activity-api) compares route data across platforms.

## FAQ

### Why does my HKWorkoutRouteQuery data handler get called more than once?

Because HealthKit returns a route's locations in batches. Apple documents the handler as a block called each time the system returns a batch of location data, which may happen one or more times, and passes a done flag that is true only when all the location data has been returned. Append each batch to one collection and process the route only when done is true.

[Permalink](https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery#faq-1)

### Why is there no route for a workout that just appeared in HealthKit?

Because route data is usually saved after the workout. Apple explains that an app must save a workout before associating route data with it, so there is a brief period when the workout exists without a route sample, and that apps may later replace the route with a smoothed version. Apple's recommendation is an anchored object query with an update handler, which delivers the route when it is added or replaced.

[Permalink](https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery#faq-2)

### How accurate are the GPS locations in a HealthKit workout route?

Apple's "Reading route data" article says locations from the HealthKit store are accurate within 50 meters but may need additional smoothing before use, for example to produce clean lines when plotting the route on a map. Each location keeps its horizontal and vertical accuracy values, which are among the CLLocation properties Apple lists as saved, so you can weigh or discard individual points.

[Permalink](https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery#faq-3)

### Can HKWorkoutRouteQuery read only part of a workout route?

Yes, from iOS 16.0 and watchOS 9.0. The initializer init(route:dateInterval:dataHandler:) limits the read to a date interval. Apple documents that an interval with no overlap returns an empty array and that a partial overlap returns only the locations from the overlapping period. On earlier versions, read the whole route and filter the locations by timestamp yourself.

[Permalink](https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery#faq-4)

### Which CLLocation fields does HealthKit keep in a workout route?

Apple says HKWorkoutRoute saves a subset of CLLocation properties, including timestamp, coordinate, altitude, speed, course, horizontalAccuracy, verticalAccuracy, speedAccuracy and courseAccuracy. Apple does not say whether that list is complete, so rely on those nine fields and do not expect any other property you recorded to come back when you read the route.

[Permalink](https://aifitnessapi.com/healthkit-queries/hkworkoutroutequery#faq-5)
