A Python project for fetching, analyzing, exporting cycling training data from and uploading plans to intervals.icu API.
intervals-icu-sync provides a set of scripts to:
- Fetch raw activity and wellness data from intervals.icu
- Analyze training quality per week using Joe Friel principles
- Export simplified summaries for a coach or ChatGPT
- Evaluate carbohydrate fueling quality per session
- Track performance metrics (FTP, VO2Max, CTL/ATL, HRV)
- Upload planned rides
For the analysis to work properly, the following conditions should be met:
-
Power meter data: Activities should contain power data. Without it, zone distribution, normalized power, and training load calculations will be incomplete or unavailable.
-
Direct sync or upload as activity source (not Strava): Activities must be synced directly from a device (e.g. Garmin Connect, Wahoo, Zwift) or uploaded manually — not via Strava. The intervals.icu API does not expose power and detailed metrics for Strava-sourced activities.
-
Carbohydrate intake logged after each ride: For fueling analysis to be meaningful, enter the amount of carbohydrates consumed (in grams) in intervals.icu after each session. This is the basis for the fueling ratio and coaching recommendations.
-
RPE logged after each ride: Enter your perceived exertion (RPE, scale 1–10) in intervals.icu after each session. It is used alongside training load and power data to assess session quality.
-
Wellness tracker connected (recommended): Linking a device such as a Garmin watch provides automatic wellness data (resting HR, HRV, sleep) that enriches the metrics analysis.
-
Body weight maintained in intervals.icu: Keep your weight up to date in intervals.icu so that calculated metrics like VO2Max are accurate.
-
Activity tags set in intervals.icu (recommended): Tag your completed activities in intervals.icu using the tag scheme described in the Coaching Logic section (e.g.
vo2max-high,lactate-treshold-moderate). Tags take priority over automatic session classification and lead to more accurate coaching output.
This project uses a structured system prompt based on Joe Friel’s training principles.
The prompt translates training theory into decision rules that allow an LLM to:
- analyze training data
- identify performance limiters
- evaluate fueling
- recommend next training steps
The combination of:
- structured data (intervals.icu)
- domain-specific prompt
- LLM reasoning
creates a lightweight but powerful coaching system:
You are an expert cycling coach following principles from Joe Friel ("The Cyclist’s Training Bible" and "Fast After 50").
Your task is to:
1. Analyze structured training data
2. Identify performance limiters
3. Make coaching decisions
4. Generate a structured training plan
---
## Athlete Context
- Age: 50+
- Goal: Increase FTP and improve long-duration climbing performance
- Event: Long climbs (60–90 minutes, e.g. alpine climbs)
- Training focus: endurance, threshold, durability
---
## Input Data
You receive:
- Weekly training summary
- Individual activities:
- duration
- training load
- power zones
- interval structure
- decoupling
- RPE
- carbohydrate usage and intake
- tags (optional)
- Metrics:
- FTP / eFTP
- CTL / ATL
- HRV / resting HR
- VO2max
---
## Training Tags (CRITICAL)
Activities and planned workouts may include tags:
Format:
"<domain>-<level>"
Domains:
- recovery
- vo2max
- lactate-treshold
- aerobic-treshold
Levels:
- low
- moderate
- high
Examples:
- "vo2max-low"
- "vo2max-moderate"
- "vo2max-high"
- "lactate-treshold-high"
- "aerobic-treshold-moderate"
---
## Tag Priority Rule
Tags OVERRIDE automatic classification:
tags > interval detection > intervals.icu classification
---
## Tag Mapping
- vo2max-* → ride_type = "vo2"
- lactate-treshold-* → ride_type = "threshold"
- aerobic-treshold-*:
- duration >= 2h → "long_ride"
- else → "endurance"
- recovery → "recovery"
---
## Weekly Structure (Friel-based)
Each week should include:
- 1× VO2max session
- 1× threshold session
- 1× long aerobic ride
- remaining sessions: endurance or recovery
---
## Fatigue / Form Calculation
Form is a RELATIVE value:
form_absolute = CTL - ATL
form_pct = (CTL - ATL) / CTL
Interpretation:
- > 0 → fresh
- -10% to 0 → transition
- -10% to -30% → optimal training zone
- < -30% → high fatigue (reduce load)
---
## Limiter Analysis
Identify the primary limiter:
- VO2max
- threshold (FTP)
- aerobic durability (decoupling)
- fueling / energy availability
---
## Decoupling Interpretation
- <5% → very good
- 5–8% → moderate
- 8–10% → high drift
- >10% → significant limitation
Use ONLY for steady efforts (Z2, long rides).
---
## Fueling Rules
- <1.5h → no fueling required
- 1.5–2h → optional
- >2h → required
Targets:
- moderate rides → 60–80 g/h
- long rides → 80–90 g/h
---
## Fueling Interpretation
- High decoupling + low carbs → fueling issue
- Good carbs + low decoupling → good durability
---
## Coaching Logic
Combine fatigue and fueling:
- optimal form + low fueling → increase carbs
- high fatigue + low fueling → reduce intensity + increase carbs
- optimal form + good fueling → proceed with key sessions
- fresh → increase load
- High fatigue + low HRV → reduce intensity
---
## Planning Rules
Generate a realistic weekly plan:
- respect fatigue (form)
- include required sessions
- do NOT increase load if fatigue is high
- prioritize limiter
---
## Tag Usage in Planning
Each workout MUST include exactly one tag:
Examples:
- VO2 sessions:
- low → short intervals (e.g. 30/15, 7×1 min)
- moderate → 2–3 min intervals (5×2 min)
- high → 3–5 min intervals (5×3 min, 4×4 min)
- Threshold:
- low → short (3×6 min)
- moderate → medium (3×10 min)
- high → long (2×18–20 min)
- Aerobic:
- low → 30–60 min
- moderate → 1–2 h
- high → 2–4 h
---
## Output Format: Training Plan JSON
You MUST return ONLY a JSON object:
{
"week": "YYYY-MM-DD",
"workouts": [
{
"date": "YYYY-MM-DDTHH:MM:SS",
"name": "string",
"duration_minutes": number,
"description": "string",
"ride_type": "vo2 | threshold | long_ride | endurance | recovery",
"tags": ["<domain>-<level>"],
"fueling": {
"carbs_per_hour": number,
"total_carbs": number
},
"workout": {
"steps": [
{
"duration": number,
"power": number
}
]
}
}
]
}
---
## Workout Construction Rules
- Always include:
- warmup (10–15 min at 0.55–0.65)
- main intervals
- cooldown (10–15 min at 0.55–0.65)
- Threshold:
- 10–20 min intervals
- 0.95–1.00 FTP
- VO2:
- 2–5 min intervals
- 1.10–1.20 FTP
- Long ride:
- steady block
- 0.60–0.70 FTP
---
## Constraints
- Output ONLY valid JSON
- No explanations outside JSON
- Steps must sum approximately to duration
- Tags must match workout structure
- Plan must be realistic for fatigue and goalsintervals-icu-sync/
├── scripts/ # Runnable entry-point scripts
│ ├── get_activities.py # Fetch activities → data/raw/
│ ├── get_metrics.py # Fetch athlete metrics → data/processed/
│ ├── analyze_week.py # Analyze current calendar week (Joe Friel)
│ ├── prepare_activities_for_coach.py # Export simplified JSON for coach/ChatGPT
│ ├── fueling_analysis.py # Analyze carbohydrate fueling quality
│ ├── fueling_planner.py # Generate carbohydrate targets per session
│ ├── upload_plan.py # Upload JSON training plan to intervals.icu
│ └── prepare_week_for_coach.py # Run all scripts in sequence
├── notebooks/
│ └── week_summary.ipynb # Interactive weekly training overview
├── src/
│ └── intervals_icu/
│ ├── __init__.py
│ ├── client.py # HTTP client (intervals.icu API)
│ └── config.py # Loads API_KEY, ATHLETE_ID from .env
├── data/
│ ├── raw/ # Raw API responses (git-ignored)
│ ├── processed/ # Derived JSON exports (git-ignored)
│ └── plans/ # Training plan JSON files
├── .env.example
├── requirements.txt
└── README.md
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activatepip install -r requirements.txtcp .env.example .env
# Edit .env and set API_KEY and ATHLETE_ID- API_KEY: found in intervals.icu under Profile → API
- ATHLETE_ID: your athlete ID, visible in the intervals.icu URL (e.g.
https://intervals.icu/athlete/i12345/...→i12345)
flowchart TD
API([intervals.icu API])
API --> GA[get_activities.py]
API --> GM[get_metrics.py]
API --> PA[prepare_activities_for_coach.py]
API --> FA[fueling_analysis.py]
API --> AW[analyze_week.py]
API --> FP[fueling_planner.py]
GA --> RAW[(data/raw/\nactivities_date.json)]
GM --> METRICS[(data/processed/\nmetrics_date.json)]
PA --> COACH_A[(data/processed/\ncoach_input_monday.json)]
FA --> FUELING[(data/processed/\nfueling_analysis_monday.json)]
AW --> SUMMARY[(data/processed/\nweek_summary_monday.json)]
FP --> FPLAN[(data/processed/\nfueling_plan_monday.json)]
RAW & METRICS & COACH_A & FUELING & SUMMARY & FPLAN --> PW[prepare_week_for_coach.py]
PW --> CONSOLIDATED[(data/processed/\ncoach_input_monday.json\nconsolidated)]
CONSOLIDATED --> COACH[[Coach\nChatGPT / Claude]]
COACH --> PLAN[(data/plans/\nweek_plan.json)]
PLAN --> UP[upload_plan.py]
UP --> CAL([intervals.icu\ncalendar])
prepare_week_for_coach.py runs all scripts above in order and then consolidates the results (metrics, week summary, activities, fueling analysis) into a single coach_input_{monday}.json.
That means: Run
python ./scripts/prepare_week_for_coach.py
to get the current version of
data/processed/coach_input_{monday}.json
for the week. Share this file with your "coach" (ChatGPT, Claude etc ...) and discuss the outcome and the plan for the week.
Ask your "coach" to create a plan for the week as JSON file. The format of the JSON is described in the system prompt above. Copy this JSON into data/plan/week_plan.json and run
python ./scripts/upload_plan.py
to upload the plan to intervals.icu
Fetches cycling activities from intervals.icu (Monday of previous week → today) and saves them to data/raw/.
python scripts/get_activities.pyOutput: data/raw/activities_{date}.json
Fetches athlete performance metrics: FTP, eFTP, W', weight, CTL, ATL, resting HR, HRV, best 5-minute power, and calculated VO2Max.
python scripts/get_metrics.pyOutput: data/processed/metrics_{date}.json
Analyzes the current calendar week (Mon–Sun) using Joe Friel training principles. Classifies sessions (VO2max / Threshold / Endurance), computes aerobic decoupling, and prints a coaching interpretation.
Also computes Form % based on CTL (fitness) and ATL (fatigue):
form_absolute = CTL − ATLform_pct = (CTL − ATL) / CTL— relative to current fitness level- Form zones:
fresh(> 0%) ·transition(0 to −10%) ·optimal(−10 to −30%) ·high_risk(< −30%) - Coaching recommendations adapt based on form zone (combined with HRV if available)
python scripts/analyze_week.pyOutput: console + data/processed/week_summary_{monday}.json
Exports a simplified JSON of this week's rides for sharing with a coach or ChatGPT. Includes duration, training load, power, RPE, interval summary, decoupling, and carbohydrate intake.
python scripts/prepare_activities_for_coach.pyOutput: data/processed/coach_input_{monday}.json
Analyzes carbohydrate fueling quality per activity and for the week. Classifies fueling based on duration (no fueling needed / optional / required), computes carbs/h and fueling ratio, detects underfueled sessions, and generates coaching recommendations.
python scripts/fueling_analysis.pyOutput: console report + data/processed/fueling_analysis_{monday}.json
Generates per-session carbohydrate intake targets based on ride type, duration, and current fatigue (Form %).
Reads from coach_input_{monday}.json (specifically the fueling_analysis.activities list, which already carries ride_type).
Target ranges by ride type:
| Ride Type | Target (g/h) |
|---|---|
| Long Ride | 80–90 |
| Threshold | 50–70 |
| VO2max | 40–60 |
| Endurance ≥ 2 h | 60–80 |
| Endurance < 2 h | 30–50 |
| Recovery | 0–30 |
When Form % < −20% (high fatigue), targets are raised by +10 g/h to offset elevated carbohydrate demand.
For each session the plan includes target g/h, total grams, and a practical strategy (gels, bottles, solid food).
python scripts/fueling_planner.pyOutput: console plan + data/processed/fueling_plan_{monday}.json
Runs all scripts in the correct order:
get_activities.py → get_metrics.py → prepare_activities_for_coach.py → fueling_analysis.py → analyze_week.py
Aborts immediately if any script fails.
python scripts/prepare_week_for_coach.pyUploads a JSON training plan to intervals.icu as planned WORKOUT events.
Reads from data/plans/week_plan.json by default (or any path passed via --plan). The plan file is git-ignored; the data/plans/ folder is tracked via a .gitkeep file.
Each entry in the JSON file must have:
date— ISO 8601 datetime string, e.g."2026-04-12T09:00:00"name— display name shown in intervals.icuduration_minutes— planned duration (integer or float)
Optional per entry: description (free-text notes), tags (list of tag strings, e.g. ["vo2max-moderate"]), steps (structured workout intervals → uploaded as a ZWO file).
Duplicate handling: before creating events, the script fetches existing WORKOUT events for the date range and indexes them by (name, date). If a match is found the existing event is updated (PUT); otherwise a new event is created (POST). Re-running the script is safe and will never produce duplicates.
# Preview without making API calls
python scripts/upload_plan.py --dry-run
# Upload the default plan
python scripts/upload_plan.py
# Upload a custom plan file
python scripts/upload_plan.py --plan data/plans/my_plan.json
# Delete all WORKOUT events for the date range, then re-upload
python scripts/upload_plan.py --clearOutput: one Created or Updated line per workout, summary of counts.
Interactive Jupyter notebook that loads the consolidated coach_input_{monday}.json and displays a structured overview of the current training week:
- Athlete Metrics: FTP, eFTP, VO2Max, W', CTL/ATL, HRV, weight — FTP values shown in W and W/kg
- Week Summary: total load, time, ride count, session types (VO2 / Threshold / Endurance), aerobic decoupling
- Form & Fatigue Analysis: CTL, ATL, Form (absolute and % relative to fitness), Form Zone, HRV — with coaching interpretation based on form zone
- Activities Table: per-ride details including power, RPE, zone distribution, decoupling, and carbohydrate data
- Zone Distribution Chart: bar charts per activity showing Z1+2 / Z3+4 / Z5+ split
- Integrated Fatigue & Fueling Analysis: combines Form % and weekly fueling quality into a single coaching interpretation with recommendation
- Fueling Analysis: per-ride fueling status, carbs/h, fueling ratio, and weekly recommendations
Run prepare_week_for_coach.py first to generate the input file, then open the notebook:
python scripts/prepare_week_for_coach.py
jupyter lab notebooks/week_summary.ipynb