TL;DR — I wrote a pair of Python scripts to convert Apple Watch workouts into FIT or TCX files that Garmin Connect can import. FIT preserves running power, stride length, and other dynamics. Heart rate resolution from Apple’s export is limited — I built a HealthKit iOS app to get the full data.

Why I needed this Link to heading

I’d been using an Apple Watch for a few years but recently sold it to CeX (a second-hand electronics shop in the UK) and picked up a Garmin Fenix 7 Pro — replaced the daily charging schedule with one charge a fortnight. I wanted to bring my workout history with me into Garmin Connect, but Apple Health locks your data in its own ecosystem — you can export it, but you get a giant XML dump and a folder full of GPX files. None of the fitness platforms can import that directly.

There are paid apps that do this conversion, but it felt like something a simple script should handle. So a few months back, I wrote one.

How the export works Link to heading

First, you export from Apple Health on your iPhone:

  1. Open Apple Health
  2. Tap your profile picture (top right)
  3. Scroll down and tap “Export All Health Data”
  4. AirDrop or email the ZIP to your Mac

You’ll get a folder with:

  • export.xml — the main dump with workout metadata, statistics, and per-second metric records
  • workout-routes/ — individual GPX files with GPS trackpoints

The tricky bit is that these are separate. Workouts live in export.xml, GPS routes are loose GPX files, and you have to link them via FileReference paths embedded in the XML.

Two output formats Link to heading

The converter supports both FIT and TCX:

DataFITTCX
GPS coordinatesYesYes
Heart rateYesYes
Distance, caloriesYesYes
Running powerYesNo
Stride lengthYesNo
Vertical oscillationYesNo
Ground contact timeYesNo

FIT (recommended) is Garmin’s native binary format. It preserves running dynamics that TCX can’t represent. Requires fit-tool via uv:

uv run convert_to_fit.py /path/to/apple_health_export

TCX is XML-based and works everywhere with zero dependencies:

python3 convert_to_tcx.py /path/to/apple_health_export

Both support --activity running to filter by type and --output to set the output directory.

The heart rate problem Link to heading

This was the most interesting rabbit hole. I went through three iterations:

Version 1: flat average. My first converter just applied the workout’s average HR to every trackpoint. Every second of a run showed the same heart rate — obviously wrong.

Version 2: per-second records. I discovered export.xml contains individual HKQuantityTypeIdentifierHeartRate records with timestamps and BPM values. My export had 358,000 of them. I parsed these into a sorted index and used binary search to match them to trackpoints.

Version 3: merged streams. Rather than interpolating or looking up metrics for each GPS trackpoint, the FIT converter now emits each data source at its own natural timestamps. GPS records have position, HR records have heart rate, power records have watts — all sorted by time. Highest resolution, no interpolation.

But there’s a catch.

Apple’s export doesn’t give you the full heart rate data Link to heading

After uploading to Garmin Connect, the HR graph looked sparse compared to what Strava showed for the same workout. I dug into the data and found that during active workouts, Apple aggregates heart rate into wide time windows. A 65-minute run might only have 23 HR records in export.xml, while Strava (which syncs via HealthKit’s API directly) shows a smooth per-second curve.

The issue is that Apple stores high-resolution HR data internally as HKQuantitySeriesSample, but the “Export All Health Data” feature flattens these into lower-resolution aggregated records. The per-second data is accessible via HKQuantitySeriesSampleQuery in the HealthKit API, but that requires an app running on the iPhone — Apple doesn’t expose HealthKit on macOS (yet — the framework exists on macOS 26 but isHealthDataAvailable() returns false).

This is a known issue that affects anyone trying to export workout HR data from Apple Health. The data is on the phone, just not in the XML export.

The HealthKit workaround Link to heading

I built a small SwiftUI iOS app that reads HealthKit directly using HKQuantitySeriesSampleQuery and serves the full-resolution data as JSON over a local HTTP server. You sideload it via Xcode with a free Apple Developer account (it expires after 7 days but that’s fine for a one-off data dump).

The app runs on your iPhone and exposes three endpoints:

# List all workouts
curl http://<iphone-ip>:8080/workouts

# Per-second HR for a specific workout
curl http://<iphone-ip>:8080/workouts/42/heart_rate

# All metrics (HR, power, speed, stride, VO, GCT)
curl http://<iphone-ip>:8080/workouts/42/metrics

This bypasses Apple’s export limitation entirely — the same per-second data that Strava gets.

Unit conversions to watch out for Link to heading

A few things that caught me out:

  • Distance: Apple uses kilometres, both FIT and TCX expect metres
  • Elevation: Apple stores elevation gain in centimetres (e.g. "1234 cm"), needs converting to metres
  • Vertical oscillation: Apple exports in cm, FIT expects mm
  • Stride length: Apple exports in metres, FIT expects mm
  • Speed: Apple exports in km/hr, FIT expects m/s
  • GPX namespaces: GPX files use the http://www.topografix.com/GPX/1/1 namespace, so you need explicit namespace handling with ElementTree

Importing to Garmin Connect Link to heading

Once you have FIT or TCX files, the import is straightforward:

  1. Go to Garmin Connect
  2. Click the “+” button and select Import Data
  3. Upload your files (can select multiple)
  4. Wait for processing

The workouts appear in your timeline with GPS maps, distance, pace, and heart rate data.

Further reading Link to heading