A Python tool to convert Zwift workout files (.zwo) to FIT workout files (.fit) for use with cycling computers and training platforms. Available as both a command-line tool and a web application.
This tool allows you to convert Zwift's proprietary workout format into the industry-standard FIT format, making your Zwift workouts compatible with Garmin, Wahoo, and other cycling computers that support structured workouts.
- Web Interface: Easy-to-use web application for drag-and-drop conversion
- Command Line Tool: Batch processing for advanced users
- Complete Workout Support: Handles all Zwift workout segments (Warmup, SteadyState, IntervalsT, Cooldown)
- Power Zone Conversion: Converts relative power zones to absolute watts based on your FTP
- Error Handling: Robust error handling with detailed feedback
- Preserves Metadata: Maintains workout names and structure
- Cross Platform: Works on Windows, macOS, and Linux
ZWO Element | Description | FIT Conversion |
---|---|---|
Warmup |
Progressive power ramp | Warmup step with average target power |
SteadyState |
Fixed power segment | Active step with target power |
IntervalsT |
Repeated work/rest intervals | Separate work and rest steps |
The easiest way to convert your ZWO files:
# Install dependencies
uv sync
# Start the web application
uv run python start_webapp.py
Then open your browser to http://localhost:5000
and:
- Upload your .zwo file
- Enter your FTP value
- Click "Convert to FIT"
- Download the converted file
For batch processing or automation:
uv run python zwift2fit.py
- Python 3.11+
- uv (recommended) or pip
git clone [repository-url]
cd zwift2fit
uv sync
git clone [repository-url]
cd zwift2fit
pip install -r requirements.txt
Start the web server:
uv run python start_webapp.py
Features:
- Drag & Drop Interface: Simply drop your .zwo file onto the upload area
- Real-time Validation: Instant feedback on file format and FTP values
- Secure Processing: Files are processed securely and cleaned up automatically
- Download Management: Converted files are automatically named and downloaded
- Error Handling: Clear error messages for troubleshooting
from zwift2fit import convert_zwo_to_fit
# Convert with default 250W FTP
convert_zwo_to_fit("my_workout.zwo", "my_workout.fit")
# Convert with custom FTP
convert_zwo_to_fit("my_workout.zwo", "my_workout.fit", ftp=275)
# Convert all .zwo files in current directory
uv run python zwift2fit.py
# The script will prompt for:
# - Input directory (default: current directory)
# - Output directory (default: same as input)
# - FTP value (default: 250W)
For production deployment:
# Build and run with Docker Compose
docker-compose up -d
# Or build manually
docker build -t zwift2fit-web .
docker run -p 5000:5000 zwift2fit-web
The web application also provides a REST API:
# Convert via API
curl -X POST http://localhost:5000/api/convert \
-F "zwo_file=@workout.zwo" \
-F "ftp=250" \
--output workout.fit
The script converts relative power zones (e.g., 0.88 = 88% FTP) to absolute watts. Set your FTP appropriately:
# For a cyclist with 250W FTP
batch_convert_zwo_to_fit("./workouts", ftp=250)
# For a cyclist with 300W FTP
batch_convert_zwo_to_fit("./workouts", ftp=300)
# Same directory (overwrites with .fit extension)
convert_zwo_to_fit("workout.zwo")
# Specific output path
convert_zwo_to_fit("workout.zwo", "converted/workout.fit")
# Batch with different directories
batch_convert_zwo_to_fit("./zwo_files", "./fit_files")
Input (.zwo file):
<workout_file>
<name>Pacing1</name>
<workout>
<Warmup Duration="420" PowerLow="0.5" PowerHigh="0.75" />
<SteadyState Duration="180" Power="0.88" />
<IntervalsT Repeat="5" OnDuration="60" OffDuration="60" OnPower="0.9" OffPower="0.7"/>
</workout>
</workout_file>
Output (.fit file):
- Warmup: 7 minutes ramping from 125W to 188W (avg 156W)
- Steady: 3 minutes at 220W (88% of 250W FTP)
- Intervals: 5x (1 min at 225W / 1 min at 175W)
your_project/
├── zwo_to_fit_converter.py # Main script
├── zwift_workouts/ # Input directory
│ ├── workout1.zwo
│ ├── workout2.zwo
│ └── ...
└── fit_files/ # Output directory
├── workout1.fit
├── workout2.fit
└── ...
The script includes robust error handling:
- Invalid XML: Skips corrupted .zwo files and continues
- Missing Elements: Uses sensible defaults for missing workout data
- File I/O Errors: Reports errors but continues batch processing
- Progress Reporting: Shows conversion status for each file
"No .zwo files found"
- Check the input directory path
- Ensure files have .zwo extension
- Verify file permissions
"Error converting [file]"
- Check if the .zwo file is valid XML
- Ensure the workout structure follows Zwift format
- Try converting the file individually for detailed error info
FIT file not recognized by device
- Verify your FTP setting is reasonable (50-500W typically)
- Check that workout duration isn't too long for your device
- Some older devices may have limited FIT workout support
To verify conversion success:
- Check that .fit files are created with reasonable file sizes
- Import a test file into your cycling computer or training software
- Verify power targets match expected values based on your FTP
The converter creates FIT files with these message types:
- FILE_ID: Identifies the file as a workout
- WORKOUT: Contains workout metadata (name, sport)
- WORKOUT_STEP: Individual workout segments with duration and power targets
Zwift Zone | Typical % FTP | Description |
---|---|---|
0.5 | 50% | Recovery |
0.7 | 70% | Endurance |
0.88 | 88% | Tempo/Sweet Spot |
0.9 | 90% | Threshold |
1.0+ | 100%+ | VO2 Max/Neuromuscular |
Feel free to submit issues, feature requests, or pull requests. Common enhancement ideas:
- Support for additional ZWO elements (Cooldown, Ramp, etc.)
- Heart rate zone targets
- Cadence targets
- GUI interface
- More sophisticated FIT file validation
This project is provided as-is for personal use. The FIT file format is owned by Garmin/ANT+.
- Python: 3.6+
- Zwift: All .zwo workout file versions
- FIT Devices: Garmin Edge, Wahoo ELEMNT, and other FIT-compatible cycling computers
- Platforms: Windows, macOS, Linux