HKWorkoutRouteQuery: reading a workout's GPS route in batches
Last verified October 3, 2026 · 5 min read
Covered here:HealthKit
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."
- Find the
HKWorkoutRoutesample attached to the workout. - Run an
HKWorkoutRouteQueryon 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.
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#
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.
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.
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 covers safer ways to show a route to other users, and GPS activity APIs compares route data across platforms.
Frequently asked questions
- 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.
- 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.
- 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.
- 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.
- 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.
Keep reading
Elsewhere on the site
Pages that share this one’s concepts and sources, from other sections.
Next steps
Was this page useful?
Independent comparison, last reviewed October 3, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All healthkit queries · by AIFitnessAPI