HealthKit Workout Lifecycle: HKWorkoutSession States And The iOS 26 Cross-Platform Surface

HKWorkoutSession is HealthKit’s workout state machine. The session moves through six states (.notStarted, .prepared, .running, .paused, .stopped, .ended), exposes lifecycle events through HKWorkoutSessionDelegate, and (since iOS 26) runs on iPhone in addition to Apple Watch1. An HKLiveWorkoutBuilder paired with the session collects samples and events incrementally; iOS 26 brought the same builder API to iPhone. The cluster’s watchOS Runtime Contract post argued that watchOS apps need a recognized session type to keep running in the background; HKWorkoutSession is one of those session types, and the lifecycle states map directly to the runtime model.

The post walks the workout lifecycle against Apple’s documentation. The frame is “what each state allows and what each transition triggers,” because workout apps that mismanage the lifecycle either lose data (transition out of running too early) or drain battery (don’t transition out of running at all).

TL;DR

  • HKWorkoutSession lifecycle: notStartedpreparedrunning → (optional pausedrunning) → stoppedended. Transitions are reported through HKWorkoutSessionDelegate.workoutSession(_:didChangeTo:from:date:)2.
  • HKLiveWorkoutBuilder is the live data accumulator paired with the workout session. It originated on watchOS in 2018 and shipped on iOS 26+, iPadOS 26+, and Mac Catalyst 26+. iPhone workouts use the same HKLiveWorkoutBuilder API as Apple Watch, with platform-specific differences in runtime model and sensor availability3.
  • The prepare() method on the session warms up sensors before startActivity(_:) runs the actual workout. Apple’s recommendation is a 3-second countdown UI between prepare() and startActivity(_:) to give heart-rate sensors and external Bluetooth devices time to connect.
  • The stopped state is transient: apps can finalize metrics in this state but cannot resume the session. Calling end() transitions to ended, which is terminal.
  • The cluster’s watchOS Runtime Contract post covers how the workout session keeps the watchOS app running through wrist-drop. The lifecycle state machine and the runtime-keep-alive contract are the two halves of the same surface.

The Six States

HKWorkoutSessionState enumerates the lifecycle2:

.notStarted. The session has been created but not prepared. Sensors are not warmed up; the app is not yet considered an active workout host. The transition to .prepared happens when the app calls prepare().

.prepared. The session has called prepare(); sensors are warming up but the workout has not started. Heart-rate monitors connect, motion sensors initialize, GPS gets a fix. The user-facing pattern is a 3-second countdown (“Get ready… 3, 2, 1, GO!”); during that window, the system has time to acquire a clean signal so the first metrics in the running state are accurate.

.running. The active workout state. The app is collecting metrics, displaying live data, and (on watchOS) keeping the screen on through the workout-active runtime contract. The transition into .running happens through startActivity(_:).

.paused. A user-paused state. The app is no longer collecting active metrics (e.g., distance) but the session is preserved; calling resume() returns to .running. The pause/resume cycle can happen any number of times within a single session.

.stopped. A transient post-workout state. The session has ended its active phase but has not been finalized; the live builder can still finalize metrics. From .stopped, calling end() transitions to .ended. The app cannot resume from .stopped.

.ended. The terminal state. The session is done; the live builder has been told to finalize; the workout is saved to HealthKit (if the app called finishWorkout(completion:) on the builder). Once in .ended, the session is no longer manipulable.

The state diagram has one specific gotcha: there is no path from .stopped back to .running. A workout that the user wants to “un-end” needs to start a new session, not resume the old one.

The Methods That Drive Transitions

HKWorkoutSession exposes the following methods for state transitions1:

  • prepare(). Transitions from .notStarted to .prepared. Warms up sensors.
  • startActivity(with: Date). Transitions from .prepared to .running. The Date parameter lets the app set the official start time (typically .now).
  • pause(). Transitions from .running to .paused.
  • resume(). Transitions from .paused back to .running.
  • stopActivity(with: Date). Transitions from .running (or .paused) to .stopped. The Date is the official end time.
  • end(). Transitions from .stopped to .ended.

The pattern from prepare() to startActivity(_:) is the warm-up window. The pattern from stopActivity(_:) to end() is the cleanup window: the live builder gets a chance to add final samples before the session terminates.

HKLiveWorkoutBuilder On watchOS And iPhone

HKLiveWorkoutBuilder is the live-data accumulator paired with the session3. The builder shipped on watchOS in watchOS 5 and was extended to iOS 26+, iPadOS 26+, and Mac Catalyst 26+. The builder’s lifecycle pairs with the session’s:

let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor

let session = try HKWorkoutSession(healthStore: store, configuration: configuration)
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)

session.delegate = self
builder.delegate = self

session.prepare()
// User taps Start after countdown
session.startActivity(with: Date())
try await builder.beginCollection(at: Date())

// During the workout, metrics flow into the builder via the data source.
// builder.collectedTypes contains the sample types being collected.
// builder.statistics(for:) returns running stats.

// User ends the workout
session.stopActivity(with: Date())
try await builder.endCollection(at: Date())
let workout = try await builder.finishWorkout()
session.end()

Three pieces glue the workout together:

  • HKWorkoutConfiguration specifies activity type and location. The activity type drives metric selection (a running workout collects pace, an indoor cycling workout doesn’t).
  • HKLiveWorkoutDataSource is the bridge from the session’s sensor configuration to the builder’s data accumulation. The data source publishes samples; the builder receives and stores them.
  • HKLiveWorkoutBuilder holds the in-progress workout’s state and finalizes the saved HKWorkout object.

The pattern is incremental: samples flow continuously during .running state; the builder integrates them into running statistics; the final finishWorkout() writes the complete workout to HealthKit.

iOS 26: Workouts On iPhone

iOS 26 brought HKWorkoutSession to iPhone with the same HKLiveWorkoutBuilder and data-source API watchOS uses4. Construction is the same; the platform-specific differences are in runtime model, sensor availability, and privacy handling rather than in API surface.

The use cases iOS 26’s workout API enables: - Phone-as-companion workout apps (the iPhone holds heart-rate-monitor data alongside the Watch session). - iPhone-only fitness apps for users without an Apple Watch, where the iPhone tracks the session through built-in sensors and connected accessories. - Cross-device session continuity: an Apple Watch session that hands off to iPhone (the user takes the Watch off but wants the iPhone to keep tracking) or vice versa.

The platform differences worth naming: - Sensor availability. iPhone has accelerometer and GPS but no built-in heart-rate sensor. Apps that need heart rate on iOS workouts pair with a Bluetooth heart-rate strap or read from a connected Apple Watch through HealthKit. - Runtime model. Apple Watch’s workout-active runtime guarantees continuous sensor access during wrist-drop. iPhone’s runtime relies on the system’s normal foreground/background lifecycle plus crash-recovery via the scene delegate (covered in WWDC 2025 session 322), which is a different guarantee shape. - Privacy and lock-screen behavior. iPhone workouts running while the device is locked require explicit configuration to keep collecting samples, since the lock screen is a stronger privacy boundary than wrist-drop.

The Delegate Protocol

HKWorkoutSessionDelegate reports state transitions and errors5:

extension WorkoutCoordinator: HKWorkoutSessionDelegate {
    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didChangeTo toState: HKWorkoutSessionState,
        from fromState: HKWorkoutSessionState,
        date: Date
    ) {
        switch toState {
        case .running:
            // workout is active
        case .paused:
            // user paused
        case .stopped:
            // finalize metrics
        case .ended:
            // workout done; cleanup
        default:
            break
        }
    }

    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didFailWithError error: Error
    ) {
        // session failed (e.g., heart rate sensor disconnected unexpectedly)
    }
}

The delegate is the single source of truth for state transitions. Apps that infer state from method calls (I called startActivity(), so we're now running) miss state changes the system applies (auto-pause when the user is stationary, auto-end when the watch is removed). The delegate-driven pattern is right.

The Runtime Contract

HKWorkoutSession is one of the session types that watchOS recognizes for keeping an app running in the background, alongside mindfulness, alarm, and audio-recording sessions6. The contract: while the session is in .prepared, .running, .paused, or .stopped state, the app continues to run; the screen wakes when the user lifts the wrist; sensors stream to the app continuously.

The cluster’s watchOS Runtime Contract post covers this in detail. The relevant point for workout apps: the lifecycle state machine is what tells watchOS “keep this app running”; transitioning to .ended releases the contract and allows the OS to suspend the app.

A practical implication: don’t end a workout session prematurely. If the user steps away from their workout for a phone call and comes back, the session should remain in .running (or be paused via pause()), not be ended. Ending and restarting loses the data and the runtime continuity.

Common Failures

Three patterns from workout app failure logs:

Skipping prepare(). Apps that call startActivity(_:) without first calling prepare() produce workouts where the first 5-10 seconds of heart-rate data are unreliable (sensor wasn’t warmed up) or missing (Bluetooth heart-rate strap hadn’t connected). Fix: always call prepare(), show a brief countdown UI, then startActivity(_:).

Calling end() from .running directly. Skipping .stopped skips the metrics-finalization window. The live builder may not have processed the final samples before the session terminates, leading to missing summary statistics. Fix: always call stopActivity(_:) first, wait for the delegate callback to confirm .stopped, then call end().

Inferring state instead of using the delegate. Apps that track local state (isWorkoutActive: Bool) and never wire the delegate miss system-driven transitions (auto-pause, auto-end on watch removal, error states). Fix: always use the delegate as the source of truth.

What This Pattern Means For iOS 26+ Apps

Three takeaways.

  1. Map the lifecycle to UI state explicitly. A workout app’s UI has obvious states: not-started, getting-ready, active, paused, summary, done. Map each one to an HKWorkoutSessionState. Don’t run UI off ad-hoc booleans; bind it to the session’s reported state through the delegate.

  2. Use prepare() plus countdown UI for any session that surfaces metrics. The 3-second warm-up is the difference between data the user trusts and data the user discounts. The cost is a small UI element; the gain is reliable metrics.

  3. iOS 26’s iPhone workout sessions need different builder code. The session API is shared; the builder side is platform-specific. Apps that share a code path between iOS and watchOS need explicit #if os(watchOS) branches or a wrapper that abstracts the difference.

The full Apple Ecosystem cluster: typed App Intents; MCP servers; the routing question; Foundation Models; the runtime vs tooling LLM distinction; three surfaces; the single source of truth pattern; Two MCP Servers; hooks for Apple development; Live Activities; the watchOS runtime; SwiftUI internals; RealityKit’s spatial mental model; SwiftData schema discipline; Liquid Glass patterns; multi-platform shipping; the platform matrix; Vision framework; Symbol Effects; Core ML inference; Writing Tools API; Swift Testing; Privacy Manifest; Accessibility as platform; SF Pro typography; visionOS spatial patterns; Speech framework; SwiftData migrations; tvOS focus engine; @Observable internals; SwiftUI Layout protocol; custom SF Symbols; AVFoundation HDR; what I refuse to write about. The hub is at the Apple Ecosystem Series. For broader iOS-with-AI-agents context, see the iOS Agent Development guide.

FAQ

Does iPhone use the same HKLiveWorkoutBuilder as Apple Watch?

Yes, since iOS 26. The same HKLiveWorkoutBuilder API ships on iOS 26+, iPadOS 26+, Mac Catalyst 26+, and watchOS 5+. The platform differences are in runtime model and sensor availability, not in builder API. iPhone workouts handle locked-screen privacy and crash recovery through the scene delegate (per WWDC 2025 session 322), which differs from watchOS’s wrist-drop runtime guarantee, but the data accumulation API is the same.

What’s the maximum workout duration?

There’s no hard duration cap. Practical limits come from battery (Apple Watch lasts ~6-8 hours under continuous workout) and storage (workouts with high-frequency data accumulate quickly). Marathon running apps (12+ hour workouts) ship today; the framework supports them.

How do I handle auto-pause?

Set HKWorkoutConfiguration.activityType to one that supports auto-pause (e.g., .running). watchOS will automatically pause and resume based on the user’s motion. The state transitions flow through the delegate; treat them the same as user-initiated pauses.

What happens if the user takes off their watch mid-workout?

The session continues in its current state (typically .running). The watchOS system will eventually end the session if the watch is off-wrist for too long; the delegate’s didFailWithError callback fires when this happens. Apps with cross-device sessions (iOS 26+) can hand off to iPhone if the user has both devices.

Should I save the workout to HealthKit?

Almost always yes. Calling builder.finishWorkout(completion:) writes the complete workout to HealthKit, which means the data appears in the Activity app, the Health app’s workout list, and any other apps the user has authorized. Skipping the save discards the data; the framework provides no recovery path.

How does this relate to other recent Apple Health additions?

iOS 26 / watchOS 26 expanded the workout API in two specific ways: first, bringing HKWorkoutSession to iPhone (covered above); second, broadening the activity type list and the auto-detection coverage. The cluster’s watchOS Runtime Contract post covers the runtime side; this post covers the lifecycle side. Together they describe the full surface for shipping a workout app.

References


  1. Apple Developer Documentation: HKWorkoutSession. The session class with state transitions, configuration, and the delegate protocol. 

  2. Apple Developer Documentation: HKWorkoutSessionState. The five state cases (.notStarted, .prepared, .running, .paused, .stopped, .ended) and their semantics. 

  3. Apple Developer Documentation: HKLiveWorkoutBuilder and HKLiveWorkoutDataSource. The live builder API (watchOS 5+, iOS 26+, iPadOS 26+, Mac Catalyst 26+) and its data source. 

  4. Apple Developer: Track workouts with HealthKit on iOS and iPadOS (WWDC 2025 session 322). The iOS 26 expansion of HKWorkoutSession to iPhone. 

  5. Apple Developer Documentation: HKWorkoutSessionDelegate. The delegate protocol with state transition and error callbacks. 

  6. Apple Developer Documentation: Background Execution on watchOS. The watchOS runtime contract describing which session types keep apps running through wrist-drop. 

Related Posts

HealthKit + SwiftUI on iOS 26: Authorization, Sample Types, and Cross-Platform Patterns

Real production patterns from Water (water tracking, HKQuantitySample) and Return (mindful sessions, HKCategorySample). …

17 min read

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 min read

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 min read