TL;DR — I wrote a Python script to convert Apple Watch workouts into TCX files that Garmin Connect (and Strava, TrainingPeaks, etc.) can import. Zero dependencies, just the standard library.

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 — 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 and statistics
  • 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.

The converter Link to heading

The script reads both sources and produces TCX files — the format Garmin uses natively.

python3 convert_apple_workouts.py /path/to/apple_health_export

You can filter by activity type:

python3 convert_apple_workouts.py /path/to/export --activity running

Output gets organised by year and month:

tcx_files/
├── 2024/
│   ├── 01/
│   │   ├── 2024-01-15_143022_Running.tcx
│   │   └── 2024-01-20_091505_Walking.tcx
│   └── 02/
│       └── 2024-02-03_182330_Running.tcx
└── no_heart_rate/
    └── 2024/
        └── 01/
            └── 2024-01-10_120000_Walking.tcx

Workouts without heart rate data get separated into their own folder. Older Apple Watch recordings sometimes lack HR, but the GPS and distance data is still useful.

The heart rate problem Link to heading

This was the most interesting limitation I hit. Apple Health exports only give you aggregate heart rate statistics — average, min, and max for the whole workout. There’s no second-by-second HR data in the export, even though the watch clearly records it.

TCX format expects a heart rate value on every trackpoint. My workaround is to apply the average HR to all points. It’s lossy — you lose the actual variation — but Garmin Connect accepts it and the summary stats are correct.

# Heart rate (interpolate from workout average if not in GPX)
if workout_data['heart_rate']:
    hr_elem = ET.SubElement(trackpoint, 'HeartRateBpm')
    hr_value = ET.SubElement(hr_elem, 'Value')
    hr_value.text = str(int(workout_data['heart_rate']['avg']))

If Apple ever includes per-second HR in their exports, this would be a straightforward fix.

Unit conversions to watch out for Link to heading

A few things that caught me out:

  • Distance: Apple uses kilometres, TCX expects metres — multiply by 1000
  • Elevation: Apple stores elevation gain in centimetres (e.g. "1234 cm"), needs converting to metres
  • Timestamps: Apple uses 2024-01-15 10:00:00 +0000, which Python’s datetime.fromisoformat() handles fine
  • 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 TCX files, the import is straightforward:

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

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

Further reading Link to heading