Skip to content
AF
Watch Apps

startMirroringToCompanionDevice: The Mirrored Workout API

Last verified October 3, 2026 · 5 min read

Mirroring is four HealthKit members. On the watch, startMirroringToCompanionDevice(completion:) starts a mirrored copy of a running HKWorkoutSession on the companion iPhone; Apple lists it for watchOS 10.0 only and says it fails on a session that has ended. On the iPhone, HKHealthStore's workoutSessionMirroringStartHandler receives that copy, and Apple says the system launches the iOS app in the background if needed, may call the handler more than once after a reconnection, and hands over a new HKWorkoutSession each time. Either side sends Data with sendToRemoteWorkoutSession(data:completion:) and receives it in workoutSession(_:didReceiveDataFromRemoteWorkoutSession:) as an array, batched while the receiving app is suspended. The iOS-side members list iOS 17.0.

Covered here:HealthKit

Why you would mirror, and how to keep one source of truth between two screens, is covered in mirroring an Apple Watch workout to iPhone. This page is the API those decisions run on, as Apple documents it on 2026-10-03.

The members, and where each runs#

MemberRuns onApple lists
HKWorkoutSession.startMirroringToCompanionDevice(completion:)WatchwatchOS 10.0
HKWorkoutSession.stopMirroringToCompanionDevice(completion:)WatchwatchOS 10.0
HKHealthStore.workoutSessionMirroringStartHandleriPhoneiOS 17.0, watchOS 10.0
HKWorkoutSession.sendToRemoteWorkoutSession(data:completion:)BothiOS 17.0, watchOS 10.0
workoutSession(_:didReceiveDataFromRemoteWorkoutSession:)BothiOS 17.0, watchOS 10.0
workoutSession(_:didDisconnectFromRemoteDeviceWithError:)BothiOS 17.0, watchOS 10.0
HKWorkoutSession.typeBothiOS 17.0, watchOS 10.0

type returns an HKWorkoutSessionType: .primary, "A primary session running on watchOS", or .mirrored, "A mirrored session, running on the companion iOS device." Branch on it in shared code instead of guessing which device you are on.

Watch side: start mirroring, then start the workout#

Apple's own example creates the session and builder, starts mirroring, and only then starts the activity:

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

try await session.startMirroringToCompanionDevice()

let start = Date()
session.startActivity(with: start)
try await builder.beginCollection(at: start)

Apple's discussion states what happens on the other side: "If your iOS app isn't running, the system launches it in the background. The iOS companion app must assign a completion handler to its HealthKit Store's workoutSessionMirroringStartHandler property to process the incoming session." It also warns: "This method fails if you call it on a workout session that ended."

Stopping is the reverse. Apple's example ends the session, calls endCollection(at:) and finishWorkout() on the builder, then stopMirroringToCompanionDevice(), after which "the system calls the workoutSession(_:didDisconnectFromRemoteDeviceWithError:) method on the iOS companion's session delegate." The general start-stop-save order is on the HKWorkoutSession lifecycle page.

iPhone side: assign the handler at launch#

// In app launch, before anything else touches the store
store.workoutSessionMirroringStartHandler = { mirroredSession in
    // Called on an arbitrary background queue
    Task { @MainActor in
        self.session = mirroredSession
        mirroredSession.delegate = self
    }
}

Three details from Apple's discussion decide how robust this is:

  • Timing. "To ensure that your app can always catch incoming mirrored workout sessions, assign this property as soon as your app launches." A handler set from a view that may not be on screen when the system launches you in the background will miss the session.
  • More than one call. "Your app may receive multiple calls to workoutSessionMirroringStartHandler. If iPhone and Apple Watch lose their connection in the middle of a workout session, Apple Watch automatically tries to reconnect. Each call has its own HKWorkoutSession instance." Replace your stored session each time; do not assume the first one is the only one.
  • Queue. "The system calls this block from an arbitrary background queue."

Disconnects invalidate the session object#

When the link drops, the mirrored side's delegate gets workoutSession(_:didDisconnectFromRemoteDeviceWithError:). Apple says: "After the system calls this method, the provided workout session is no longer valid, and you can no longer use it." The error is nil when no error caused the disconnect. Then: "If the primary workout session is still running, it automatically tries to reconnect. If successful, the companion iOS device calls the workoutSessionMirroringStartHandler block again, passing in a new, valid HKWorkoutSession instance." In practice, clear your reference on disconnect, show the phone as disconnected rather than ended, and let the handler repopulate it.

Sending data both ways#

sendToRemoteWorkoutSession(data:completion:) (and its async throws form) takes a single Data. Apple notes "You can send data from either the HKWorkoutSessionType.mirrored or HKWorkoutSessionType.primary session," so the same call carries live metrics from watch to phone and commands such as "next interval" from phone to watch. The encoding is yours; Apple's sample archives an object with NSKeyedArchiver. Our recommendation is a small versioned Codable message type, so a phone build and a watch build from different releases can still talk.

The receiving method gets [Data], not one value, and Apple explains why: "In iOS, your app can go into the background and become suspended. When suspended, HealthKit gathers the data coming from the remote session. When the app resumes, HealthKit sends an array containing all the data objects it has accumulated." It adds that "there might be several minutes between delegate calls." On the watch, the session keeps the app running, but "the system can temporarily suspend the app — for example, if the app uses an excessive amount of CPU in the background," with the same caching on resume. Two design consequences:

  • Process the whole array in order. The last element is the newest state; earlier ones are history you may still need (a lap marker, a pause).
  • Never treat a message as "now". Put a timestamp in every message, because delivery can lag by minutes.

What the API does not decide for you#

Apple's "Running workout sessions" article gives a companion rule that predates mirroring but still applies: if the user starts on the watch and tries to end in the iOS app, "the iOS app should instruct the user to end the workout in your watchOS app. Otherwise, Apple Watch workout continues to run, which can lead to accidentally saving invalid data." A command sent with sendToRemoteWorkoutSession that the watch acts on is one way to do that. The API itself does not define a remote "end" command, so the protocol, acknowledgement and timeout are yours to design.

Apple's sample "Building a multidevice workout app" (iOS 17.0, watchOS 10.0, WWDC23 session 10023) also notes it must run on physical devices and that the watch target's WKCompanionAppBundleIdentifier must match the iOS app's bundle ID. In our experience that key is the first thing to check when the start handler never fires. The rest of a two-device test plan is in testing watch apps.

When the iPhone runs its own session#

From iOS 26, Apple lists HKWorkoutSession(healthStore:configuration:) and HKLiveWorkoutBuilder on iPhone too, so a phone can run a primary session of its own without a watch. That is a different product from mirroring. If you support both, branch on session.type and keep exactly one primary session per workout.

Frequently asked questions

Why does workoutSessionMirroringStartHandler fire more than once during one workout?
Because Apple Watch reconnects after a dropped link. Apple's discussion of the handler says your app may receive multiple calls, that if iPhone and Apple Watch lose their connection mid-session the watch automatically tries to reconnect, and that each call has its own HKWorkoutSession instance. The disconnect delegate method also says the old session object is no longer valid after it fires. So each handler call should replace your stored session and reattach its delegate, and your UI should treat a disconnect as a gap, not as the end of the workout.
Can the iPhone send commands to the watch over a mirrored workout session?
Yes, as data. Apple documents sendToRemoteWorkoutSession(data:completion:) on iOS 17.0 and watchOS 10.0 and states you can send from either the mirrored or the primary session, and the other side receives it in workoutSession(_:didReceiveDataFromRemoteWorkoutSession:). HealthKit does not define what the bytes mean, so pause, next interval or end requests are a message format you design. Include a timestamp and an identifier in each message, because Apple says delivery to a suspended app is batched and can be several minutes apart.
Which OS versions does Apple list for startMirroringToCompanionDevice?
watchOS 10.0, and only watchOS: mirroring is started from the watch. The receiving side, HKHealthStore's workoutSessionMirroringStartHandler, along with sendToRemoteWorkoutSession(data:completion:) and the mirrored-session delegate methods, lists iOS 17.0 and watchOS 10.0. If your watch app supports watchOS versions below 10, the mirroring path needs an availability check and a fallback that does not depend on it.

All 55 questions in Watch Apps →

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