Native Shell
Native Shell: AI-generated UI, native in your app, on every platform. Apps on iOS, Android, macOS, Windows and Linux, GPUI apps and web pages load signed, sandboxed TSX modules at run time and draw th
Connect any GenAI tool to your intervals.icu data — includes MCP server, Python scripts, system prompts and coaching logic based on Joe Friel.
Developer toolsPython · Jupyter NotebookGitHub @rbrandsSelf-installOwner unconfirmed
Paste into Claude or ChatGPT for installation instructions suited to your environment.
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Installable projects can be verified without a deployment domain. Use GitHub sign-in for a personal repository, or the repository verification file above for an organization repository.
Ask about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A Python project and MCP server for fetching, analyzing, and exporting cycling and running training data from intervals.icu — and for uploading AI-generated training plans back to the calendar. The project includes ready-to-use system prompts and a coaching logic library with domain knowledge based on Joe Friel's training principles, so you can connect your AI assistant and start coaching conversations immediately.
intervals-icu-sync provides two ways to work with your intervals.icu training data:
Both expose the same coaching workflow:
For the analysis to work properly, the following conditions should be met:
Power or heart-rate data: Cycling activities work best with power data. Running activities can be analyzed from heart-rate and zone data; when no running power is present, power-specific fields are omitted from coach exports.
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 session: 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 session: Enter your perceived exertion (RPE, scale 1–10) in intervals.icu after each session. It is used alongside training load, power when available, and heart-rate data to assess session quality.
Optional but very helpful: Use the description in "Notes" after an activity to comment on your training given the "AI Coach" more context.
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-threshold-moderate). Tags take priority over automatic session classification and lead to more accurate coaching output.
Training plan created in intervals.icu using the Target Generator (recommended): Create a training plan in intervals.icu via the Target Generator (Plans → Target Generator). This places PLAN events (mesocycle blocks, e.g. Base / Build / Peak) and TARGET events (weekly TSS or time targets) in your calendar. get_training_plan.py reads these events and adds the current phase tag, when present, and weekly targets — including the following week's targets — to the coach input. TSS targets are exposed as weekly_load_target; if time_target_hours is also present, treat it as an upper time cap. If weekly_load_target is missing, weekly_time_target_hours becomes the weekly target. It also propagates day-level constraints (for example Sick/Travel days) when available. Race events categorized as RACE_A, RACE_B, or RACE_C in intervals.icu are included for the current and next week so the coach can taper and protect the race date. Without an active PLAN phase, TARGET entries or day constraints can still populate the training plan section; it is empty only when there is no phase, target, or constraint data for those weeks.
The coaching system is split across two directories:
prompts/system_prompt.md — The base system prompt for the LLM (ChatGPT, Claude, etc.). It contains a placeholder block:
## Athlete Profile
<<INSERT ATHLETE / DISCIPLINE BLOCK HERE>>
Before passing the prompt to the coach, copy the contents of the matching discipline_*.md file into that block:
| File | Athlete type |
|---|---|
discipline_climber.md | Climber / FTP-focused athlete |
discipline_criterium.md | Criterium racer / W' and repeatability focus |
discipline_roadrace.md | Road racer / aerobic durability and FTP focus |
discipline_marathon.md | Mountain marathon (L'Etape du Tour, Ötztaler) / ultra-long endurance focus |
discipline_fitness.md | Fitness / health athlete without a race goal / VO2max maintenance, consistency, no taper |
discipline_cross.md | Cyclocross racer / starts, low-speed accelerations, race weekends as key sessions |
discipline_zwift.md | Zwift racer / start block, VO2max, threshold under constant pressure, race-frequency cap |
This keeps the base prompt stable while allowing the athlete profile to be swapped out per use.
coach-logic/ — Modular dxocumentation of the coaching domain knowledge:
Canonical tag domains are vo2max, lactate-threshold, aerobic-threshold,
race-specific, and recovery with dose levels low, moderate, high;
strength alone uses phase levels adaptation, transition, maximum,
maintenance. Legacy strength is not a canonical tag.
| File | Content |
|---|---|
coaching-principles.md | Training philosophy and foundational coaching principles based on Joe Friel |
interpretation-rules.md | Data interpretation thresholds and classifier rules (form, fueling, limiter detection) |
decision-process.md | How interpreted data is turned into concrete weekly planning actions |
training-zones.md | Power, HR and RPE zone definitions used by the coach |
input-schema.md | Description of the JSON input schema passed to the coach |
workout-library.md | Workout catalog by domain and dose level, including canonical tags, breakaway sets, and criterium microbursts |
The combination of:
prompts/system_prompt.md)creates a lightweight but powerful coaching system. The full system prompt is maintained in prompts/system_prompt.md.
Both MCP servers expose check_athlete_data(athlete_data). Pass the JSON
string already returned by prepare_week_data, or an existing coach-input
export. The tool makes no API calls, stores nothing, and gives no training
recommendations. Python callers can use
intervals_icu.data_quality.check_athlete_data(payload_dict) directly.
The MCP response is a native JSON object in structuredContent, with a
declared output schema, not a serialized JSON string in a result field.
It contains checks (eight areas, each with area, status,
explanation, evidence, and context), overall, non_ok_count,
suggested_fix, report_text, and the input date/window metadata.
report_text contains one line per area plus one summary line; explanations
are in English and can be translated by the caller without changing statuses.
| Area | Deterministic rule |
|---|---|
| Season plan / macrocycle | OK when entries for both the current and next week have a non-empty phase; Warning when only one week has a phase; Missing when neither does. Week entries and day constraints without a phase do not count as a macrocycle. |
| FTP consistency | Compare metrics.rolling_ftp and metrics.eftp with metrics.ftp, using absolute percentage differences relative to FTP. Below 10%: OK; 10–20% inclusive: Warning; above 20%: Missing with an explicit inconsistent flag. Missing or non-positive values also produce Missing; the worst comparison determines the area status. |
| W' consistency | Apply the same thresholds to metrics.rolling_w_prime, relative to metrics.w_prime. |
| Wellness | OK if any of metrics.resting_hr, metrics.hrv, or metrics.sleep_secs is non-null; otherwise Missing. |
| Activity density | Count activities dated from current_date - lookback_days through current_date, both inclusive, matching the preparation pipeline's lower boundary while excluding future entries. At least 3: OK; 1–2: Warning; 0: Missing. |
| Activity feedback | In that same window, consider only non-WeightTraining activities (RPE and carbohydrate intake are optional for strength, decision D6). OK if every applicable activity has non-null rpe and non-null carbs_ingested_g (zero counts as recorded), or if only strength activities are present. Missing if there are no activities at all, or if RPE or carbohydrate data is absent for every applicable activity; otherwise incomplete feedback is Warning. Evidence reports applicable_activities and strength_activities_excluded. |
| Historical targets | OK if any training_load_history entry has a non-null weekly_load_target (including zero); otherwise Missing. |
| Strength tagging | Warning (never Missing) when is true. Lists eligible exported activities from the previous ISO Monday through (date, name, id, and phase/source when available), excluding activity-/plan-resolved sessions and sessions with . For source , suggests or when the current phase is null. For , says "counted as - tag the session if it was a different phase", using the session's . when the flag is false or the strength summary is absent. Evidence: , , , , window dates. The listing cannot extend beyond the exported activity window; the coach itself never mentions tagging. |
If 0.95 * metrics.power_profile.p20min.watts is more than 20% below FTP,
the FTP check adds context about the power-profile window and potentially
missing sustained maximal efforts. This is not a data error and does not
change the status or overall rating.
Overall completeness is Good for zero non-OK areas, Partial for one or
two, and Limited for three or more. The single suggested fix uses a stable
priority: Missing before Warning, then season plan, wellness, FTP, W',
feedback, activity density, historical targets, and strength tagging. This is a data-quality
priority, not a training prescription; complete data requires no fix.
When strength tagging is the suggested fix, the summary says: "Untagged strength
sessions are counted as the current phase. Tag a session only if it was a
different phase." For unresolved sessions (phase_source: "none"), it instead
says: "Tag the session with a strength- tag so it counts toward the phase."
Mixed inferred/unresolved findings include both instructions. The same wording
is used in suggested_fix and the final line of report_text; per-activity
phase evidence and the suggested tag remain available.
current_date, week_starting, and a positive integer lookback_days are
required. Dates must use YYYY-MM-DD, and week_starting must be the Monday
of current_date. Missing optional sections are assessed as missing data;
malformed JSON, wrong field types, invalid dates, and non-finite numbers
raise an explicit MCP tool error rather than returning a completeness rating.
Unrelated additive fields are ignored.
The consistency prompt prefers this tool when available and retains manual fallback rules for clients without it.
For a local console report, run python scripts/check_athlete_data.py.
It reads the current week's data/processed/coach_input_{monday}.json by
default; it does not fetch fresh data or silently select an older export.
Use --input to check another consolidated export, or --json for the full
structured report. Raw API activity lists are not valid inputs.
The tools in this project can be used in three different ways, depending on your technical comfort level and setup preferences.
Run the Python scripts locally and exchange JSON files with your AI coaching tool manually.
What you do each week:
prepare_week_for_coach.py to pull all data from intervals.icu and produce a single coaching input file.validate_plan.py to validate the plan JSON against the upload schema.upload_plan.py to push the plan to intervals.icu.Best for: Users who want full control, prefer no external dependencies, or want to understand the tooling in detail.
Details: See Setup, Data Flow and Scripts further below.
Use the publicly hosted MCP server at intervals-mcp.training-architect.com. No local Python environment required — connect your AI assistant directly to the server via the Model Context Protocol.
What you do each week:
Best for: Users who prefer a managed, zero-install experience without running any local scripts.
Step-by-step guide: docs/gen_ai_setup_step_by_step.md
Use the live web app at training-architect.com for an end-to-end coaching workflow without installing the MCP server or running local scripts.
The same curated dataset and plan workflow can also be used with any GenAI tool:
This is a complete fallback when MCP cannot or should not be configured, for example because of technical restrictions or licensing. API users can perform the same flow with GET /api/dataset, POST /api/validate, and POST /api/upload; the OpenAPI documentation is available at training-architect.com/swagger.
Detailed article: Use Any GenAI Coach with intervals.icu Without MCP
Step-by-step setup guide: docs/gen_ai_setup_step_by_step.md
Project repository: github.com/rbrands/training-architect
intervals-icu-sync/
├── scripts/ # Runnable entry-point scripts
│ ├── get_activities.py # Fetch activities → data/raw/
│ ├── get_metrics.py # Fetch athlete metrics → data/processed/
│ ├── get_training_plan.py # Fetch active training plan → data/processed/
│ ├── analyze_week.py # Analyze current calendar week (Joe Friel)
│ ├── prepare_activities_for_coach.py # Export simplified JSON for coach/ChatGPT
│ ├── prepare_planned_workouts_for_coach.py # Format planned workouts → data/processed/
│ ├── fueling_analysis.py # Analyze carbohydrate fueling quality
│ ├── fueling_planner.py # Generate carbohydrate targets per session
│ ├── validate_plan.py # Validate week_plan JSON against upload schema
│ ├── check_plan_tss.py # Check plan TSS against deterministic step-based reference
│ ├── check_athlete_data.py # Check consolidated athlete-data quality locally
│ ├── upload_plan.py # Upload JSON training plan to intervals.icu
│ ├── wbal_analysis.py # Compute W'bal time series from power stream
│ ├── prepare_week_for_coach.py # Run all scripts in sequence
│ ├── get_latest_activities.py # Print compact activity list (mirrors MCP get_latest_activities)
│ ├── get_activity_streams_sampled.py # Fetch sampled activity streams for one activity locally
│ ├── list_workouts.py # List workout library entries with duration, TSS and tags
│ ├── generate_week_data_schema.py # Generate JSON Schema for consolidated week_data payload
│ └── mcp_server.py # FastMCP server exposing data as tools/resources
├── prompts/
│ ├── system_prompt.md # System prompt for the AI coach (LLM instructions)
│ ├── discipline_climber.md # Athlete profile block: climber / FTP focus
│ ├── discipline_criterium.md # Athlete profile block: criterium / W' focus
│ ├── discipline_cross.md # Athlete profile block: cyclocross / starts, race weekends
│ ├── discipline_fitness.md # Athlete profile block: fitness / health, no race goal
│ ├── discipline_marathon.md # Athlete profile block: mountain marathon / ultra-long endurance
│ ├── discipline_roadrace.md # Athlete profile block: road race / durability focus
│ └── discipline_zwift.md # Athlete profile block: Zwift racing / start block, race cap
├── coach-logic/
│ ├── coaching-principles.md # Underlying training philosophy and core principles (Joe Friel)
│ ├── interpretation-rules.md # Interpretation thresholds for fatigue, durability, fueling and limiters
│ ├── decision-process.md # Decision flow to turn interpretation into weekly training actions
│ ├── training-zones.md # Power, HR and RPE zone definitions
│ ├── input-schema.md # JSON input schema description for the AI coach
│ ├── workout-library.md # Workout catalog by domain and dose level (with canonical tags)
│ └── skill/
│ └── SKILL.md # Foundry skill manifest used by foundry-agent/deploy_agent.py
├── contracts/
│ └── week-data/
│ ├── week-data.schema.json # JSON Schema for consolidated week_data payload
│ ├── WeekDataDto.cs # C# DTOs for consolidated week_data payload (System.Text.Json)
│ └── WeekDataJson.cs # C# JSON serialize/deserialize helper for WeekDataDto
├── foundry-agent/
│ ├── agent.yaml # Microsoft Foundry prompt agent definition (MCP + coach-logic knowledge)
│ ├── agent-staging.yaml # Staging overrides with isolated Foundry resources
│ ├── deploy_agent.py # Deploy/update agent versions and refresh vector store
│ ├── invoke_agent.py # Invoke agent locally (single-turn and chat mode)
│ ├── requirements.txt # Foundry-agent specific Python dependencies
│ ├── infra/
│ │ ├── main.bicep # Foundry control-plane IaC (account, project, models, RBAC)
│ │ ├── main.bicepparam # Parameter template for Foundry infra deployment
│ │ ├── main.json # Generated ARM template from main.bicep
│ │ └── main.local.bicepparam # Local parameter file for Foundry infra deployment (git-ignored)
│ └── README.md # Setup: structured inputs, vector store, MCP server, deployment
├── docs/
│ ├── 2026-05 Next Level intervals-icu.pdf # Webinar slides (German)
│ ├── 2026-05 Next Level intervals-icu Step by Step.pdf # Step-by-step setup guide (English)
│ ├── dfa_a1.md # DFA alpha 1 in-ride aerobic-threshold marker reference (English)
│ ├── gen_ai_setup_step_by_step.md # GenAI coach and MCP setup guide (English)
│ ├── genai_without_mcp.md # Any-GenAI workflow without MCP (English)
│ ├── webinar_notes.md # Webinar companion guide (German)
│ ├── prompt_library.md # Copy-paste prompts for ChatGPT / Claude (DE + EN)
│ ├── strength_training.md # Planned strength-training integration and data rules
│ └── training_readiness.md # Training Readiness traffic-light explainer (English)
├── 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_quality.py # Shared deterministic athlete-data quality checks
│ ├── dfa_a1.py # Shared DFA alpha 1 analysis of in-ride R-R intervals (hrv stream)
│ ├── strength.py # Canonical strength phase tags and phase mapping
│ ├── training_readiness.py # Shared readiness traffic-light calculation
│ ├── week_data_schema.py # Pydantic models for consolidated week_data JSON schema
│ └── wbal.py # Shared W'bal computation (Skiba differential model)
├── data/
│ ├── raw/ # Raw API responses (git-ignored)
│ ├── processed/ # Derived JSON exports (git-ignored)
│ └── plans/ # Training plan JSON files
├── tests/
│ ├── test_data_quality.py # Data-quality thresholds and MCP tool regression tests
│ ├── test_dfa_a1_analysis.py # DFA a1 computation, activity gate and weekly aerobic-threshold aggregate
│ ├── test_next_best_training_prompt.py # Compact recommendation and upload prompt contract
│ ├── test_strength_week_scope.py # WeightTraining scope, summary and week phase resolver
│ └── test_upload_plan_regressions.py # Regression tests for upload_plan.py and ZWO generation
├── .env.example
├── .pre-commit-config.yaml # Git hook config (strips Jupyter outputs before commit)
├── CHANGELOG.md # Project change history (Keep a Changelog format)
├── requirements.txt
├── VERSION # Current project version (SemVer)
├── Run-ProjectPython.ps1 # Run any Python command with .venv (Python 3.14)
├── Start-Chainlit.ps1 # Start Chainlit with .venv312 (Python 3.12)
├── start_mcp_server.ps1 # Start/stop the MCP server in SSE mode (Windows PowerShell)
├── config.example.ps1 # Template for deployment/config values (copy to config.ps1)
├── setup.ps1 # Helper script: generate local params and optionally push GitHub secrets
├── webservice/ # MCP server deployed as Azure App Service (see webservice/README.md)
│ ├── mcp_server.py # ASGI webservice host for FastMCP endpoints
│ ├── oauth_provider.py # OAuth 2.0 provider for hosted MCP authentication
│ ├── context.py # Request context/auth helper utilities
│ ├── Check-Deployment.ps1 # Post-deployment smoke checks for webservice endpoints
│ ├── requirements.txt # Webservice-specific Python dependencies
│ ├── infra/
│ │ ├── main.bicep # App Service infrastructure as code for hosted MCP server
│ │ ├── main.local.bicepparam # Local parameter file for webservice infra deployment
│ │ └── modules/ # Reusable Bicep modules used by webservice infra
│ └── README.md # Webservice deployment and operations guide
└── README.md
Foundry infrastructure manages gpt-4.1-mini plus gpt-5.6-luna, gpt-6-luna,
and gpt-6-sol. The additional models are configured in
foundry-agent/infra/main.bicep via additionalModelDeployments; agent model
selection remains separate. See the Foundry infrastructure guide
for versions, capacities, and the manual preview/apply workflow.
The Foundry agent deployment workflow
deploys only the staging agent for same-repository pull requests targeting main.
Pushes to main and manual runs deploy both production and staging; fork PRs
are skipped. See the agent CI/CD guide
for Azure OIDC requirements.
macOS note: if
python/pipare not available (or point to Python 2), usepython3andpip3for all commands below.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
Enable the notebook output stripping hook:
pre-commit install
This project includes .pre-commit-config.yaml with nbstripout, so notebook output and execution-count-only changes are removed automatically at commit time.
This repository includes two helper scripts so you can keep Python 3.14 for the main project and Python 3.12 for Chainlit in parallel:
# Run project scripts/tools with .venv (Python 3.14)
.\Run-ProjectPython.ps1 scripts\prepare_week_for_coach.py
# Start Chainlit with .venv312 (Python 3.12)
.\Start-Chainlit.ps1 -Port 8013 -Watch
On macOS/Linux (PowerShell Core), run:
pwsh ./Start-Chainlit.ps1 -Port 8013 -Watch
If .venv312 does not exist yet:
py -3.12 -m venv .venv312
.\.venv312\Scripts\python.exe -m pip install -r foundry-agent/requirements.txt
macOS/Linux equivalent:
python3.12 -m venv .venv312
./.venv312/bin/python -m pip install -r foundry-agent/requirements.txt
cp .env.example .env
# Edit .env and set INTERVALS_API_KEY and ATHLETE_ID
Only needed if you use the MCP server with a Cloudflare tunnel (or other reverse proxy):
FASTMCP_ALLOWED_HOST=your-tunnel-hostname.example.comSet this to the public hostname of your tunnel (e.g.
intervals-icu-mcp-local.my-brands.com). The MCP server uses it to accept incoming requests that carry thatHostheader. Leave it unset if you only run the MCP server locally (no tunnel).
This project follows Semantic Versioning.
VERSIONCHANGELOG.md## [Unreleased] in CHANGELOG.md.VERSION to the new release version.Unreleased to a dated ## [x.y.z] - YYYY-MM-DD section.main; changes to VERSION trigger the staging deployment.Swap Staging → Production workflow to publish the release.v0.1.0).flowchart TD
API([intervals.icu API])
API --> GA[get_activities.py]
API --> GM[get_metrics.py]
API --> GTP[get_training_plan.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)]
GTP --> TPLAN[(data/processed/\ntraining_plan_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)]
TPLAN --> PPW[prepare_planned_workouts_for_coach.py]
PPW --> PLANNED[(data/processed/\nplanned_workouts_monday.json)]
RAW & METRICS & COACH_A & FUELING & SUMMARY & FPLAN & PLANNED --> 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, planned workouts) 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
get_activities.pyFetches cycling, running, and weight-training activities from intervals.icu (Monday of previous week → today) and saves them to data/raw/.
Included activity types are Ride, MountainBikeRide, and GravelRide (plus VirtualRide for indoor/platform rides), Run, TrailRun, and VirtualRun, and WeightTraining.
Missing or invalid icu_training_load values are treated as 0 during filtering.
Cycling activities with a training load above 20 are always kept. Running activities are kept whenever intervals.icu supplies icu_training_load, even if the value is below 20. WeightTraining activities are always kept; load is optional. STRAVA-source duplicates remain excluded. Tagged ride/run activities below the cycling threshold are kept only when they carry at least one usable metric (training load, average or normalized power, heart rate, or zone times), so incomplete placeholder entries are dropped. If icu_zone_times is missing, running activities fall back to icu_hr_zone_times for the Z1/Z2, Z3/Z4, and Z5+ distribution.
For activities with power data, the coach export also includes the activity FTP (activity_ftp) and activity-specific eFTP (activity_eftp) supplied by intervals.icu; these are separate from the current athlete Metrics FTP. activity_eftp is estimated from the best efforts of that single ride and is a lower-bound estimate: rides without near-maximal efforts always yield values well below the athlete's real FTP, so low per-ride values carry no signal (see coach-logic/interpretation-rules.md, Per-Activity eFTP).
Every exported activity also carries the post-activity fitness state reported by intervals.icu (icu_ctl/icu_atl) as ctl, atl and form_absolute (CTL − ATL), so the CTL/ATL/TSB trajectory across the export window is visible per session and not only as the current snapshot in week_summary.
python scripts/get_activities.py
Output: data/raw/activities_{date}.json
get_metrics.pyFetches athlete performance metrics: the current Ride FTP from the athlete settings, FTP classification by age/sex (including W/kg), VO2Max, VO2Max classification by age/sex, eFTP, the percentage delta between FTP and eFTP (ftp_eftp_delta_pct), W', rolling W', the configured Ride power zone upper bounds as % FTP (power_zones_pct, used as the Zone 2 reference for the DFA a1 aerobic-threshold estimate), weight, CTL, ATL, resting HR, HRV, and the 42-day power profile. The consolidated training_load_history also includes four completed weekly CTL/ATL snapshots with the same explicit form fields as week_summary, alongside the weekly TSS target and actual load.
In consolidated coach_input payloads, ctl and atl are surfaced under week_summary (together with form fields) to keep readiness metrics in one place. The consolidation step also writes an explainable daily traffic light to week_summary.training_readiness, combining form, hard-session recency, HRV, resting HR, and sleep. Every exported activity includes is_hard_session, using the same classification as hard-session recency. A resolved WeightTraining activity in the maximum strength phase counts as hard. A session with TSS at least 50% above current CTL counts as hard regardless of its training distribution. In addition, HIIT and Polarized sessions always count as hard, while Threshold sessions require at least 50 TSS or RPE 7+ so short, easy rides with incidental Z3/Z4 time do not reset hard-session recency.
Also exports wellness_trends for weight, resting_hr, and hrv with only: current, avg_7d, avg_prev_7d, and trend_7d.
Weight values are normalized to kilograms (kg). If the athlete account stores weight in pounds, get_metrics.py converts it to kg before writing outputs.
Power Profile Type: Adds a heuristic rider type estimate to metrics.power_profile using the 42-day power curve (p15s, p1min, p5min, p20min, curve_slope). Exposed fields are type, type_key, heuristic_score, type_scores, and type_method (currently heuristic_v1).
VO2Max Classification: Classification by age group (13–19, 20–29, 30–39, 40–49, 50–59, 60+) and sex (male/female). Categories: very poor, poor, average, good, very good, excellent. Includes delta to next category and thresholds for range.
The estimate uses 16.6 + 8.87 * p5min W/kg. Classification also includes
p5min_pct_ftp (one decimal), estimate_representative (false below
100% FTP, "likely_understated" from 100% to below 105%, true at or above
105%; thresholds use the unrounded ratio), and power_profile_period_days
(copied from power_profile.period_days). Missing p5min or usable FTP
leaves the percentage and flag null. Unless the flag is boolean true,
the coach must not use the VO2Max value/category as limiter or performance
trend evidence and states once that a maximal 5-minute effort is missing.
Week-data schema version is 1.6.0; the generated C# contract uses
JsonElement? for the boolean/string/null flag.
python scripts/get_metrics.py
Output: data/processed/metrics_{date}.json
analyze_week.pyAnalyzes the current calendar week (Mon–Sun) using Joe Friel training principles. Classifies Ride sessions (VO2max / Threshold / Endurance), computes aerobic decoupling (for Base/Pyramidal/Threshold rides ≥ 90 min only), and prints a coaching interpretation.
total_training_load, number_of_activities, total_time_hours, and longest_activity_hours aggregate Ride and Run activities and exclude WeightTraining. The ride_*, run_*, and strength_* load/session/time fields report each sport separately. Training-distribution sessions, decoupling, aerobic-threshold estimates, and days_since_* remain Ride-only; long-ride counting for fueling decisions is handled in fueling_analysis.weekly_summary.number_of_long_rides.
Also computes Form % based on CTL (fitness) and ATL (fatigue), and writes ctl/atl alongside form fields into week_summary, including weeks with no qualifying rides yet:
form_absolute = CTL − ATLform_pct = (CTL − ATL) / CTL — relative to current fitness leveltransition (> +20%) · fresh (+5% to +20%) · grey_zone (−10% to +5%) · optimal (−30% to −10%) · high_risk (< −30%)Session distribution semantics in week_summary:
vo2_sessions: Ride sessions classified as VO2max.threshold_sessions: sessions classified as threshold.endurance_sessions: catch-all bucket for sessions that are neither VO2max nor threshold.
This includes long steady activities that are not classified as VO2max/threshold.days_since_last_hiit: calendar days since the last activity classified as HIIT.days_since_last_polarized: calendar days since the last activity classified as HIIT or Polarized.days_since_last_hard_session: calendar days since the last activity classified as HIIT, Polarized, or Threshold.The three recency fields are computed from all exported activities in the lookback window relative to the current date, not only from the current ISO week, so a hard session on the previous weekend stays visible early in the new week.
Long-ride counting for fueling is tracked separately in
fueling_analysis.weekly_summary.number_of_long_rides.
Current rule: duration >= 2.5 h and no structured intervals.
Aerobic threshold estimate (DFA a1): When at least one exported activity in coach_input_{monday}.json carries a valid dfa_a1_summary that actually crossed DFA a1 = 0.75 (see prepare_activities_for_coach.py), the summary gains aerobic_threshold_estimate with rides_used, threshold_windows_total, the power_at_0_75 / hr_at_0_75 across those rides (weighted by each ride's threshold_windows, so rides with more evidence near the threshold count more), the athlete's configured Zone 2 ceiling in watts (z2_upper_power, z2_upper_source = athlete_zones from metrics.power_zones_pct, or ftp_75pct as fallback), and the relative delta_pct between the two. The key is omitted when no ride crossed the threshold; no multi-week trend is computed.
python scripts/analyze_week.py
Output: console + data/processed/week_summary_{monday}.json
prepare_activities_for_coach.pyExports a simplified JSON of cycling, running, and WeightTraining activities in the active lookback window (LOOKBACK_DAYS, default: 7) for sharing with a coach or ChatGPT. Cycling and running exports include duration, training load, HR (avg/max), RPE, interval summary, zone distribution from icu_zone_times or fallback icu_hr_zone_times, compact interval HR analysis (interval_hr_analysis with hr_start_avg, hr_end_avg, hr_drift_pct, hr_power_decoupling), decoupling, and carbohydrate intake. WeightTraining exports use a reduced field set only: id, type, date, name, duration_hours, training_load, ctl, atl, form_absolute, the three load deltas, rpe, tags, and notes; no power, heart-rate, zone, interval, decoupling, W'bal, power-curve, DFA, fueling, or weather fields are included, and none of those analyses are run. For cycling and running activities, power, FTP, polarization index, power-curve, and W'bal fields are included only when an activity has power data. The HR interval summary is computed only from eligible WORK intervals (minimum 120 seconds and minimum 95% FTP intensity). Decoupling labels are only classified for Base/Pyramidal/Threshold activities ≥ 90 min; shorter or high-intensity activities show "limited durability signal".
Ride-level W' fields use icu_w_prime; if that value is missing or 0, the scripts automatically fall back to icu_rolling_w_prime. These fields are omitted for activities without power data.
WeightTraining activities are always kept within the export window, except STRAVA duplicates; load is optional. Missing training_load stays null on the activity, contributes zero to weekly strength load sums, and does not affect tag/date-based phase counts or hard-session classification.
For longer rides that were ridden as aerobic sessions (≥ 60 min, HR data, not a race, no lactate-threshold-*/vo2max-*/race-specific-* tag, and either a canonical aerobic-threshold-*/recovery-* tag or a Base/Pyramidal training_distribution — the tag has priority because outdoor terrain often pushes an easy ride's power distribution into Z3/Z4) whose intervals.icu stream_types include hrv, the script analyzes the recorded R-R intervals and adds a compact (DFA alpha 1: artifact and coverage quality gate, mean a1, share of time above 0.75 / below 0.5, and power and HR at a1 ≈ 0.75 as a per-ride aerobic-threshold estimate, plus relative to the activity FTP; in-ride durability remains a reading). At most the three newest qualifying rides are analyzed per run; threshold workouts, HIIT and fast group rides are deliberately skipped so that a low aerobic share never reads as a warning for an intentionally hard ride. The key is omitted entirely for rides without an stream or outside the gate, and with a is written when the strap data fails the quality gate. Requires HRV logging to be enabled on the head unit (e.g. Garmin Edge: ) and a chest strap that transmits R-R intervals. Streams are fetched once per ride with a type filter and shared between the W'bal and DFA a1 analyses.
Activities in the exported list are sorted by date/time with the newest activity first.
Each activity also includes , , and directly after their corresponding values: its CTL, ATL, and absolute form minus those of the chronologically previous activity. For example, CTL 45.2 after 44.1 gives ; ATL 52.1 after 53.3 gives . Deltas are rounded to one decimal place; each delta is when either value is missing or there is no earlier exported activity.
python scripts/prepare_activities_for_coach.py
Output: data/processed/coach_input_{monday}.json
fueling_analysis.pyAnalyzes carbohydrate fueling quality per activity and for the active lookback window (LOOKBACK_DAYS, default: 7). Classifies fueling based on duration (no fueling needed / optional / required), computes carbs/h and fueling ratio, detects underfueled sessions, and generates coaching recommendations. WeightTraining activities are excluded from per-activity and weekly fueling analysis and receive no fueling field in consolidated activities. Long rides in this context are counted with the script's long-ride rule and surfaced as weekly_summary.number_of_long_rides. Rides whose interval summary contains standalone efforts of 30 s or less (e.g. 6x 6s) with little Z5+ time are classified as endurance_with_sprint.
python scripts/fueling_analysis.py
Output: console report + data/processed/fueling_analysis_{monday}.json
fueling_planner.pyGenerates per-session carbohydrate intake targets based on tag-derived ride type, duration, and current fatigue (Form %).
Reads from coach_input_{monday}.json (specifically activities[].fueling plus workout tags; if a legacy ride_type is present it is still honored).
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.py
Output: console plan + data/processed/fueling_plan_{monday}.json
prepare_planned_workouts_for_coach.pyReads the most recent training_plan_*.json and extracts the planned workouts and RACE_A/RACE_B/RACE_C events for the current and next ISO week. Simplifies each workout to the fields relevant for coaching (date, name, type, duration, planned load, description, zone distribution, step structure) and saves races separately under each week's race_events field.
python scripts/prepare_planned_workouts_for_coach.py
Output: data/processed/planned_workouts_{monday}.json
prepare_week_for_coach.pyRuns all scripts in the correct order:
get_activities.py → get_metrics.py → get_training_plan.py → prepare_activities_for_coach.py → prepare_planned_workouts_for_coach.py → fueling_analysis.py → analyze_week.py
Aborts immediately if any script fails. During consolidation it computes
week_summary.training_readiness for today's or the next session. The object
contains status, score, confidence, recommendation, reasons, safety
vetoes, and the contribution of every available input signal. Missing optional
wellness values lower confidence instead of counting as negative readiness.
Strength activities are retained in the activity list without an attached
fueling object. Consolidation adds strength_phase, strength_phase_source
(activity, planned, planned_week, inferred, none), and
strength_tagging_reminder_needed (source none/inferred and no planned
WeightTraining event in that ISO week) to exported strength activities by ID.
The original activity tags remain unchanged. Consolidation fetches a projected
16-week history of completed WeightTraining sessions plus planned WeightTraining
WORKOUT events, resolves canonical phase tags, and exports the compact
week_summary.strength plus metadata on already-exported activities; the raw history
stays internal and is not included in coach or MCP payloads. The compact summary
includes sessions_per_week_last_4 (all sessions per completed ISO week, oldest
first, aligned with training_load_history),
untagged_sessions_last_16_weeks (only sessions with phase_source none),
inferred_sessions_last_16_weeks (only source inferred), and
tagging_reminder_needed (none or inferred sessions in the current/previous ISO week
with no planned strength event in that same week; surfaced as the strength_untagged
warning of check_athlete_data, never by the coach). Activity phase tags win,
then same-date planned tags, then tags from the nearest planned WeightTraining
event in the same ISO year/week; equal-distance ties prefer the earlier date.
As a final fallback, an untagged session inherits the phase of the most recent
earlier activity-/plan-resolved session when the gap is less than 21 days
(inferred). Inferred sessions count toward phase_session_count and continue
the run but do not anchor further inference. Before the first resolved session,
or at a gap of 21 days or more, the source remains none. Phase runs stop at
gaps of at least 21 days between sessions; 21 days without a session also resets
the current phase. phase_overdue is true only above 6 adaptation, 3 transition,
or 12 maximum sessions (false for maintenance/unset phase), with no automatic
phase switch inside a run.
Python also resolves the strength phase deterministically at the start of each
week: uses the history before this Monday, while
also counts this week's completed sessions and planned
WeightTraining events through Sunday. No history, or a gap of 21 days or more,
starts adaptation. A run advances once it reaches its minimum (adaptation 4 →
transition 2 → maximum 8 → maintenance). In Build/Peak weeks
(), athletes with at least one full adaptation run switch
to maintenance; otherwise the week uses one beginner adaptation session.
/ is 1 for
maintenance and beginner build, otherwise 2. is one of
, , , , , or
. Strength planning is on by default; the coach skips it only
when the request says or the athlete says
so in chat.
Planned events are fetched through Sunday of the current week, including the
full ISO week containing the history start. The raw history remains internal.
Week-data schema version is 1.6.0.
The local and hosted MCP tools use the same derivation.
Set to change the default history window, including
the window used for the untagged count. In the coach activity payload, is true for
WeightTraining only when tagged ; the generic CTL-based TSS
threshold does not apply to strength sessions.
python scripts/prepare_week_for_coach.py
generate_week_data_schema.pyGenerates contract artifacts for the consolidated week_data / coach_input payload returned by prepare_week_data.
By default it writes both the JSON Schema and the C# DTO classes.
python scripts/generate_week_data_schema.py
python scripts/generate_week_data_schema.py --schema-output ./contracts/week-data/week-data.schema.json
python scripts/generate_week_data_schema.py --dto-output ./contracts/week-data/WeekDataDto.cs
Output:
contracts/week-data/week-data.schema.jsoncontracts/week-data/WeekDataDto.csget_latest_activities.pyReads the most recent coach_input_{monday}.json (run prepare_week_for_coach.py first) and prints a compact JSON summary in the same format that the webservice MCP tool get_latest_activities returns. Useful for locally inspecting or testing the compact activity list without the full webservice stack.
Returns 9 fields per activity: id, date, name, duration_hours, training_load, avg_hr, max_hr, rpe, tags.
python scripts/get_latest_activities.py # last 10 activities (default)
python scripts/get_latest_activities.py --limit 5 # last 5 activities
Output: JSON to stdout
get_activity_streams_sampled.pyFetches a single activity's time-series streams from intervals.icu, applies optional time/distance filtering, and down-samples the selected streams to a manageable size. This mirrors the MCP tool get_activity_streams_sampled and is designed for local testing of power/HR stream analysis without using the hosted stack.
python scripts/get_activity_streams_sampled.py --id i175554112
python scripts/get_activity_streams_sampled.py --id i175554112 --streams time,distance,heartrate --max-points 250
Output: JSON to stdout with compact stream arrays under streams.
list_workouts.pyLists all workouts from the athlete's intervals.icu workout library and shows the key planning fields per workout: folder, duration, TSS, and tags. By default it prints a table to stdout; with --json it returns normalized JSON output. The MCP equivalent additionally returns library_workout_id so generated plans can reference the exact stored workout.
python scripts/list_workouts.py
python scripts/list_workouts.py --json
Output: table or JSON to stdout
get_training_plan.pyFetches the athlete's currently active training plan from intervals.icu (if one is assigned). Exports active phase(s), current and next-week TSS or time targets, upcoming race events with their RACE_A/RACE_B/RACE_C priority, and day-level constraints (e.g. Sick/Travel/Unavailable) derived from calendar NOTE and availability markers. For NOTE events, explicit training_availability values (for example LIMITED) take precedence over label keyword inference. If a constrained day has a max duration in intervals.icu, it is exported as day_constraints[].max_training_time_hours.
The consolidated coach input also includes a top-level training_load_history array for the last four completed calendar weeks. Each entry contains week_starting, the Ride weekly_load_target when available, server-aggregated Ride total_training_load, and achievement_pct. The current week is excluded because its target and actual load already belong to week_summary. Historical load is queried directly from the intervals.icu athlete-summary API and does not depend on older local export files.
python scripts/get_training_plan.py
Output: data/processed/training_plan_{date}.json
wbal_analysis.pyFetches the power stream for one or more activities from intervals.icu and computes the W'bal (anaerobic energy reserve) time series using the Skiba differential model:
P − CP joules per secondW'bal += (W' − W'bal) × (1 − e^(−1/τ)) where τ = W' / (CP − P̄_sub_cp)icu_w_prime and icu_ftp in the raw activity dataPer-activity output includes:
| Field | Description |
|---|---|
wbal_min_j | Minimum W'bal reached (joules) |
wbal_usage_pct | Maximum depletion as % of W' |
seconds_below_30pct | Seconds with W'bal < 30 % of W' |
seconds_below_10pct | Seconds with W'bal < 10 % of W' |
wbal_depletion_events | Number of times W'bal drops below 40 % and recovers above 50 % |
wbal_recovery_ratio | Average ratio of W' recovered vs. W' depleted per event (0–1, null if no events) |
Plus the full second-by-second W'bal series.
# All qualifying rides from the latest raw activities file
python scripts/wbal_analysis.py
# Single activity by id
python scripts/wbal_analysis.py --id i143131711
# Show a matplotlib plot (requires matplotlib)
python scripts/wbal_analysis.py --id i143131711 --plot
Output: data/processed/wbal_{activity_id}.json
mcp_server.pyFastMCP server that exposes the training data pipeline and plan upload as MCP tools. Consolidated week data includes the daily traffic light under week_summary.training_readiness. It also offers a compact latest-activities method for clients that truncate large tool outputs and a sampled activity-stream tool for sub-km analysis. The stream tool supports optional stream selection, bounding by time or distance, and a fixed point cap (default 300) to keep outputs compact. See MCP Server Integration for setup and usage.
check_athlete_data.pyChecks a consolidated coach-input export using the same deterministic eight-area data-quality rules as the MCP tool.
# Check the current week's consolidated export (no fresh API fetch)
python scripts/check_athlete_data.py
# Check a specific export
python scripts/check_athlete_data.py --input data/processed/coach_input_2026-10-05.json
# Print the full structured report as JSON
python scripts/check_athlete_data.py --json
Default output: eight compact status lines plus overall completeness and a
single data-quality fix. --json prints the full report with evidence and
context. Inferred strength sessions use their exported strength_phase_source
and strength_phase to report the counted phase, with tagging needed only if
it differed; unresolved sessions are instructed to add a phase tag so they count
toward the phase. No file is
written. Exit code 0 means the check ran successfully,
including Partial or Limited results; 1 means the file could not be read
or the input was invalid. Use the consolidated export, not a raw activity
list. To refresh input first, run prepare_week_for_coach.py.
check_plan_tss.pyReports each workout's stated Ride TSS next to the deterministic step-based calculation and checks whether completed plus planned Ride TSS (using the computed values) is within the weekly Ride target tolerance. Run and WeightTraining entries are listed but excluded.
# Check a generated plan file against the target load
python scripts/check_plan_tss.py --plan /tmp/plan.json --load-target 300 --tolerance-pct 10
# Use the default tolerance of 10%
python scripts/check_plan_tss.py --plan data/plans/my_plan.json --load-target 300
The script accepts either a bare workout array or an object shaped like { "workouts": [...] }. Ride types are Ride, VirtualRide, MountainBikeRide, and GravelRide; absent or null activity_type defaults to Ride. For ride workouts with library_workout_id, the tss value is kept as-is. For generated rides with steps (top-level or workout.steps), TSS is recomputed from the step durations and power values; stated_tss, computed_tss, and mismatch are reported per workout, but a mismatch is informational only (listed in notes) and does not make the result invalid, because upload_plan.py overwrites the stated values on upload. The weekly total uses computed_tss. WeightTraining entries have computed_tss 0; other non-ride entries keep their stated TSS.
Output: a JSON object with workouts, week, valid, issues, and notes keys.
Only the weekly target deviation (and an exceeded optional Ride time budget) sets
valid to false and adds an entry to issues.
Each workout has counted to indicate Ride-target inclusion. week.planned_tss
is retained and equals planned_ride_tss; planned_other_tss sums excluded
entries. total_tss = planned_ride_tss + completed_tss. Pass completed Ride
TSS with --completed-tss for the current week and zero for a future week.
week.planned_ride_hours and planned_other_hours report the matching duration
split. Optional --weekly-time-target-hours checks an upper budget for planned
Ride hours only (no lower-bound or percentage tolerance); time_within_budget
reports the result and a violation makes valid false.
The local MCP check_plan_tss(plan_json, load_target, tolerance_pct=10, completed_tss=0, athlete_data="") returns the same JSON. Optional athlete_data
is a coach-input JSON string; it derives completed TSS from
week_summary.ride_load, falling back to total_training_load only when
ride_load is absent. Omit athlete data and keep completed TSS zero for a
future week.
The local MCP TSS tool also accepts weekly_time_target_hours explicitly;
it does not infer the intended week from athlete data.
upload_plan.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", "race-specific-low"]), activity_type (intervals.icu activity type; defaults to Ride), steps (structured workout intervals → uploaded as a ZWO file), and library_workout_id (copies the exact stored library workout, including its type and workout_doc, instead of generated steps).
Use activity_type for non-ride planned activities and for indoor rides. Supported upload values are Ride, VirtualRide, MountainBikeRide, GravelRide, Run, TrailRun, VirtualRun, and WeightTraining. The coach sets VirtualRide for workouts it expects to be ridden indoors (derived from the recent activities[].type majority or an explicit statement in the chat; see coach-logic/decision-process.md, "Training Environment") and omits the field for outdoor rides. Run and WeightTraining entries can be uploaded without steps; in that case the event is created from duration, notes, and tags only.
Before uploading, generated rides with steps (no library_workout_id) have tss, any TSS: <n> mention in the description, and duration_minutes recomputed from the steps (TSS = Σ duration_seconds/3600 × (power_pct_ftp/100)² × 100, rounded); WeightTraining entries get tss 0. Each correction is printed as an Adjusted: line. Library workouts and other entries are uploaded unchanged. This applies to both MCP upload_week_plan tools, which call this script.
{
"workouts": [
{
"date": "2026-09-05",
"name": "Easy Run",
"duration_minutes": 30,
"activity_type": "Run",
"tags": ["recovery-low"]
},
{
"date": "2026-09-06",
"name": "Strength",
"duration_minutes": 45,
"activity_type": "WeightTraining",
"tags": ["strength"]
}
]
}
Plan JSON Schema: contracts/week-plan/week-plan.schema.json (supports both accepted top-level formats: bare workout array or { "week": "...", "workouts": [...] }).
Field handling on upload: name is truncated to 127 characters. Descriptions and notes, including appended fueling guidance, are sent in full without local truncation. Structured steps are uploaded as a ZWO workout file rather than embedded in the description.
Tag handling: the plan format should always use tags, including for a single tag. For backward compatibility, upload_plan.py still accepts a legacy single tag string and internally treats it as a one-item tags list.
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 --clear
Output: one Created or Updated line per workout, summary of counts.
validate_plan.pyValidates a plan JSON file against the upload schema before calendar upload.
WeightTraining entries require tags containing exactly one canonical phase
tag (strength-adaptation, strength-transition, strength-maximum, or
strength-maintenance), duration 30-75 minutes, absent or empty steps
(including nested workout.steps), and no library_workout_id. The legacy
tag field remains tolerated but does not replace the required tags array.
Schema errors include JSON locations and readable messages.
The local MCP validate_week_plan(plan_json="", max_errors=10, athlete_data="")
also checks at most one ride and one WeightTraining entry per calendar date;
a ride and strength session on the same date are allowed. Optional
athlete_data is a coach-input JSON string: entries of every sport are rejected
on dates marked training_allowed: false in
week_summary.training_plan[].day_constraints. Availability checks are skipped
when no constraints are supplied. These date-level semantic checks are in the
local MCP tool; the standalone script validates the schema.
The local MCP validator checks weekly_time_target_hours in each supplied
training-plan week against planned rides in that ISO week only. On LIMITED
days, max_training_time_hours caps the sum of ALL entries on the date.
For example, 60 minutes Ride plus 45 minutes WeightTraining requires 1.75
hours: a 1.75-hour day cap passes, while 1.5 or 1.0 hours fails. Time checks
are skipped when the respective target or constraints are absent. After schema
validation, the result includes week.planned_ride_hours and
week.planned_other_hours for the supplied plan.
Default inputs:
data/plans/week_plan.jsoncontracts/week-plan/week-plan.schema.json# Validate default plan file
python scripts/validate_plan.py
# Validate a custom plan file
python scripts/validate_plan.py --plan data/plans/my_plan.json
# Print more than 10 validation errors
python scripts/validate_plan.py --max-errors 25
Exit code behavior:
0 when the plan is valid1 when the plan/schema file is missing, JSON is invalid, or validation failsnotebooks/week_summary.ipynbInteractive Jupyter notebook that loads the consolidated coach_input_{monday}.json and displays a structured overview of the current training week:
"no durability data" if no eligible rides exist)ctl, atl, and training_readiness are read from week_summary)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
docs/genai_without_mcp.mdLanguage: English
A standalone, blog-ready guide to using the curated intervals.icu dataset with any GenAI coach without MCP. Covers the clipboard workflow, plan validation and explicit upload confirmation, the public HTTP API, curl examples, security, and limitations.
docs/training_readiness.mdLanguage: English
A blog-ready explanation of the Training Readiness traffic light, including its five input domains, scoring thresholds, hard-session definition, safety overrides, confidence levels, worked example, output structure, and limitations.
docs/dfa_a1.mdLanguage: English
Single reference for the DFA alpha 1 in-ride aerobic-threshold marker: what the metric is and how it differs from wellness HRV, head-unit prerequisites (HRV logging, chest strap), the computation and quality gates, the dfa_a1_summary and aerobic_threshold_estimate fields, the coach's reading rules and division of labour with decoupling, limitations, configuration constants, and literature references.
docs/2026-05 Next Level intervals-icu.pdfLanguage: German (Deutsch)
Slides of webinar „Next Level intervals.icu – Vom Datenchaos zur Coaching-Entscheidung" (Mai 2026).
docs/2026-05 Next Level intervals-icu Step by Step.pdfLanguage: English
Step-by-step setup guide accompanying the webinar. Walks through the full installation and configuration of the intervals.icu AI coach integration — from API key setup to MCP server and ChatGPT connection.
docs/webinar_notes.mdLanguage: German (Deutsch)
Webinar companion guide for „Next Level intervals.icu – Vom Datenchaos zur Coaching-Entscheidung".
Covers the core workflow: fetching data from intervals.icu, enriching it with the AI coach logic, generating a weekly training plan, and uploading it back to the calendar. Intended as a readable walkthrough for participants who want to understand or reproduce the setup without a live demo.
docs/prompt_library.mdLanguages: German (Deutsch) + English
A curated collection of copy-paste prompts for use with ChatGPT, Claude, or any other AI assistant. Each prompt is provided in both German and English.
Covers seven use cases:
| Prompt | Purpose |
|---|---|
| Single Workout Analysis | Deep-dive analysis of the most recent session (quality, fueling, W' usage, recovery) |
| Weekly Analysis | Full weekly review: metrics, load balance, training quality, fueling, and limiter |
| Training Plan Generation | Generate a structured training plan for the coming week |
| Fueling Analysis | Dedicated fueling strategy review with pattern detection and recommendations |
| Metrics & Wellness Summary | Compact overview of current performance metrics, HRV, sleep, and form state with load recommendation |
| Data Consistency Check | Completeness and consistency report of the athlete data without coaching interpretation |
| Next Best Training | At most 100 words plus exactly one main workout recommendation: at most one ride and optionally one same-day strength session (preliminary tomorrow if already trained); easier alternative in prose only, no automatic upload |
coach_prompt_next_best_training returns the same {"workouts": [...]} upload
format as a weekly plan, between BEGIN_UPLOAD_JSON / END_UPLOAD_JSON markers:
at most one ride and optionally one same-day WeightTraining session when the
strength rule yields a phase other than none. The easier alternative remains prose-only.
Upload options (only after an explicit user request):
python scripts/validate_plan.py --plan <file>, then
python scripts/upload_plan.py --plan <file> if validation succeeds.save_week_plan(plan_json=...), then
validate_week_plan() on the saved file, and only on success upload_week_plan().validate_week_plan(plan_json=...), then only on
success upload_week_plan(plan_json=...) with the same JSON. No save step is
needed or available.The user reviews the recommendation and requests upload explicitly, just as with a weekly plan. The prompt reuses the system prompt's output contract and TSS rules. Rest or insufficient data produces prose only, not a zero-duration calendar event.
All prompts assume the MCP server connection or a manually pasted coach_input_{monday}.json as data source.
contracts/week-data/WeekDataDto.csC# DTO set (System.Text.Json) for the consolidated week_data payload generated by prepare_week_data / coach_input.
The classes use JsonPropertyName for snake_case mapping and JsonExtensionData for forward-compatible extra fields.
The file is generated by scripts/generate_week_data_schema.py.
contracts/week-data/WeekDataJson.csC# utility helpers for deserialization/serialization with safe default JsonSerializerOptions for WeekDataDto.
contracts/week-data/week-data.schema.jsonGenerated JSON Schema artifact for the same week_data contract, intended for CI/build consumption and cross-language validation.
Regression tests live in tests/. They use Python's built-in unittest framework and require no real credentials — dry_run=True prevents any API calls.
Run all tests:
python -m unittest discover -s tests -v
Run a specific test file:
python -m unittest tests/test_upload_plan_regressions.py -v
tests/test_upload_plan_regressions.pyCovers regression cases for upload_plan.py and the ZWO generation logic:
| Test | What it checks |
|---|---|
test_steps_to_zwo_accepts_seconds_and_percent_fields | _steps_to_zwo produces correct ZWO XML (Duration, Power) from duration_seconds / power_pct_ftp fields |
test_create_activity_sends_raw_workout_doc_unchanged | create_activity sends a native library workout_doc unchanged without converting it to ZWO |
test_upload_plan_dry_run_supports_top_level_steps | upload_plan accepts steps directly at the plan-entry level |
test_upload_plan_dry_run_supports_nested_workout_steps | upload_plan accepts steps nested under a workout key |
test_upload_plan_uses_library_workout_content | upload_plan resolves library_workout_id and copies the stored name, duration, description, tags, and workout structure |
tests/test_library_workout_listing.pyCovers MCP library workout IDs and validation of library-backed week-plan entries without generated steps.
week_summary.strength.tagging_reminder_neededWeightTrainingcurrent_datestrength_tagging_reminder_needed: falsenonestrength-<current_phase>strength-adaptationinferredstrength_phaseOKtagging_reminder_neededuntagged_activitiessuggested_tagcurrent_phasedfa_a1_summarypower_at_0_75_pct_ftpdecouplinghrvvalid: falsereasonctl_deltaatl_deltaform_absolute_deltactl_delta: 1.1atl_delta: -1.2nullphase_current_weekphase_next_weektraining_plan[].phaserecommended_sessions_current_weekrecommended_sessions_next_weekphase_reason_*continueadvance_min_reachedno_historygap_resetbuild_guardbeginner_buildStrength phase for this plan: noneprepare_week_dataSTRENGTH_HISTORY_WEEKSis_hard_sessionstrength-maximum