HKWorkoutSession Lifecycle in Swift: Start, Stop, Save
Last verified October 3, 2026 · 5 min read
Covered here:HealthKit
The architecture of a watchOS workout app (what the session owns, what the builder owns, why session state should drive your UI) is covered in the anatomy of a watchOS workout app. This page is the call sequence, method by method, as Apple's HealthKit reference and its "Running workout sessions" article document it on 2026-10-03.
Before the first call: authorization and capabilities#
Apple's article is specific. "For workout sessions, you must request permission to share workout types", so HKQuantityType.workoutType() goes in the share set, with heart rate, active energy and distance in the read set as your app needs them. Two capabilities follow: "Workout sessions require the Workout processing background mode. If your app plays audio or provides haptic feedback during the workout session, you must also add the Audio background mode."
Audio has a second rule worth knowing if you build spoken coaching: Apple notes that workout apps can play short clips in the background, but "an active workout session must be running; any attempt to play background audio outside a workout session are invalid." Cues belong inside the session's lifetime; the watch speaker and Bluetooth routing are covered in audio cues on the wrist.
The state machine Apple gives you#
HKWorkoutSessionState has six cases: .notStarted, .prepared ("The session is ready but not yet running"), .running, .paused, .stopped and .ended. Your HKWorkoutSessionDelegate hears every transition through workoutSession(_:didChangeTo:from:date:), and Apple says all the delegate's methods are required and are called "on an anonymous serial background queue", so hop to the main actor before touching UI.
One sentence in that delegate method's discussion changes how you write it: "If your application is suspended, the delegate receives this call after the application resumes. This means you may receive the notification long after the state changed." Use the date parameter, not Date(), when you record when a pause or stop happened.
Starting#
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor
let session = try HKWorkoutSession(healthStore: healthStore,
configuration: configuration)
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(healthStore: healthStore,
workoutConfiguration: configuration)
session.delegate = self
builder.delegate = self
let start = Date()
session.startActivity(with: start)
try await builder.beginCollection(at: start)
The initializer throws, and Apple says why: it "can throw an exception if the configuration is invalid." Apple also says to "Use the same configuration object for the workout session and the live data source." The completion-handler form beginCollection(withStart:completion:) and the async form beginCollection(at:) are the same method.
prepare() is documented simply as "Prepares the workout session", and the matching state is .prepared, "ready but not yet running." Our suggestion is to call it when a countdown screen appears, so the session already exists when the user's start tap arrives.
While it runs#
The data source does the collecting. Apple's article gives the example that "an outdoor running session collects and saves activeEnergyBurned, basalEnergyBurned, heartRate, and distanceWalkingRunning samples." You read them through the builder delegate:
func workoutBuilder(_ workoutBuilder: HKLiveWorkoutBuilder,
didCollectDataOf collectedTypes: Set<HKSampleType>) {
for case let type as HKQuantityType in collectedTypes {
let stats = workoutBuilder.statistics(for: type)
// publish stats to the UI on the main actor
}
}
workoutBuilderDidCollectEvent(_:) fires for events; read workoutEvents.last. You can add your own samples with add(_:completion:) and events with addWorkoutEvents(_:completion:). To add a quantity type to what the data source gathers, Apple's mirroring example calls enableCollection(for:predicate:) on the HKLiveWorkoutDataSource, which Apple says "Begins automatically calculating statistics for samples that match the quantity type and predicate."
pause() and resume() are requests. The state change arrives through the delegate, and that is when the UI should change.
Stopping and saving, in Apple's order#
Apple's sequence is stop, wait, end collection, finish, end:
session.stopActivity(with: Date())
// HKWorkoutSessionDelegate
func workoutSession(_ workoutSession: HKWorkoutSession,
didChangeTo toState: HKWorkoutSessionState,
from fromState: HKWorkoutSessionState,
date: Date) {
guard toState == .stopped else { return }
Task {
try await builder.endCollection(at: date)
let workout = try await builder.finishWorkout()
workoutSession.end()
}
}
What each call does, in Apple's words: endCollection(withEnd:completion:) "sets the workout's end date and deactivates the builder"; finishWorkout(completion:) "saves the workout and its associated data to the HealthKit store". Apple adds that you must call endCollection before finishWorkout, and that finishWorkout "returns nil if finishing the workout succeeded but the workout sample is not available because the device is locked." A nil result is not a failure. Do not show an error for it.
discardWorkout() "Stops the collection of data and discards the current results without saving the workout." Apple's Human Interface Guidelines give a concrete use: "If a session ends a few seconds after it starts, either discard the data automatically or ask people if they want to record the data as a workout."
When the session ends without you#
Apple states that "Apple Watch runs one workout session at a time. If a second workout starts while your workout is running, your HKWorkoutSessionDelegate object receives an HKError.Code.errorAnotherWorkoutSessionStarted error, and your session ends." That case and the other session-ending errors are covered in HealthKit workout session errors.
Recovering after a crash#
Apple documents a recovery path: "If your app crashes during a workout session, the system calls your extension delegate's handleActiveWorkoutRecovery() method when the app relaunches." There you call recoverActiveWorkoutSession(completion:) on the health store (an async throws form returns HKWorkoutSession?). Then comes the step people miss: "As soon as you receive the session object, you must access its builder and set up your data source and delegates again." Factor the setup above into one function that both a fresh start and a recovery call.
Background behavior#
With an active session, Apple says the app "continues to run throughout the entire workout session, even when the user lowers their wrist or interacts with a different app," keeps receiving sensor data, and can alert with audio or haptics. The limit is CPU: "If your app uses an excessive amount of CPU while in the background, watchOS may suspend it." The wider runtime picture is in Apple Watch background execution.
On iPhone and iPad: versions to check#
The HKWorkoutSession class page lists iOS 17.0, but the members that make a session collect data list later versions. init(healthStore:configuration:), associatedWorkoutBuilder(), HKLiveWorkoutBuilder, HKLiveWorkoutDataSource and recoverActiveWorkoutSession(completion:) all list iOS and iPadOS 26.0, with watchOS 5.0. Apple's sample "Building a workout app for iPhone and iPad" (associated with WWDC25 session 322) targets iOS 26. Below iOS 26 the iPhone side of a watch workout is the mirrored session, covered in the HKWorkoutSession mirroring API.
The newest addition at the time of writing is zone tracking: workoutBuilder(_:didUpdateWorkoutZone:) and HKLiveWorkoutZoneUpdate list iOS and watchOS 27.0.
Frequently asked questions
- Why should I wait for the stopped state before calling endCollection on the workout builder?
- Because that is the order Apple documents. Its Running workout sessions article says to call stopActivity(with:) and, after the session has transitioned to the stopped state, call the builder's endCollection(withEnd:completion:) and finishWorkout(completion:), and finally end() on the session. Its sample does this inside workoutSession(_:didChangeTo:from:date:) and passes the delegate's date as the end date. Calling endCollection from the button handler instead races the session's own transition, and the end date you record is when your code ran rather than when the session stopped.
- finishWorkout returned nil without an error. Did the workout save?
- Apple says yes. The finishWorkout(completion:) discussion states that the function returns nil if finishing the workout succeeded but the workout sample is not available because the device is locked. Treat nil with no error as a completed save, and show your summary from the statistics you already collected during the session. Our advice is not to prompt the user to retry, which invites them to record the same workout twice.
- How does a watchOS app get its workout back after crashing mid-session?
- Apple documents that if the app crashes during a workout session, the system calls the extension delegate's handleActiveWorkoutRecovery() method when the app relaunches. In it, call recoverActiveWorkoutSession(completion:) on your HKHealthStore; HealthKit attempts to restore the previous session and returns a new session object or an error. Apple then says you must access the recovered session's builder and set up your data source and delegates again, so keep that setup in one function shared by the fresh-start and recovery paths.
- Can an iPhone app run its own HKWorkoutSession with a live workout builder?
- From iOS 26, according to Apple's availability metadata. The HKWorkoutSession class lists iOS 17.0, but init(healthStore:configuration:), associatedWorkoutBuilder(), HKLiveWorkoutBuilder and HKLiveWorkoutDataSource all list iOS and iPadOS 26.0, and Apple's Building a workout app for iPhone and iPad sample targets iOS 26. Apple's session overview also notes that iPhone and iPad have no heart rate sensor, so collecting heart rate there requires pairing an external sensor. Below iOS 26 the iPhone joins a watch workout as a mirrored session.
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 watch apps · by AIFitnessAPI