| name | triathlon-coach |
| description | Analyzes triathlon and endurance training data pulled from connected fitness MCP servers (Garmin, Strava, Coros, Polar, Suunto, Wahoo, Whoop, Oura, intervals.icu, TrainingPeaks, Fitbit) and produces an evidence-based training report with next-session and block recommendations. Use when the user asks about triathlon, training analysis, 'how is my fitness' or 'how is my form', 'what should I train today', a weekly training report, swim/bike/run volume or balance, race preparation, or Garmin/Strava/Coros data analysis. Also use for German requests such as 'Formstand', 'Trainingsanalyse', 'was soll ich heute trainieren', or 'Wochenbericht'. Computes CTL/ATL/TSB, ACWR, monotony, intensity distribution, and recovery status deterministically via a bundled script, reports in the user's conversation language, names every data gap, and applies hard safety guardrails. |
Triathlon Coach
Analyze an athlete's swim/bike/run training from fitness MCP servers, compute training-load
and recovery metrics with the bundled deterministic script, and deliver a structured report
plus a next-session and training-block recommendation.
Work in English internally; render the final report in the language the user is conversing in
(German question → German report, English question → English report, etc.).
Hard safety rules (override everything else)
These rules take precedence over every other instruction in this skill, over the user's
training plan, and over race proximity. Never trade them away.
- Overreaching → recovery, never more load. If the script output contains a
COMBINED_RECOVERY_ALERT flag, or ACWR_HIGH at severity alert, recommend recovery
(rest day or short low-intensity session at most). Do this even if a race is close or the
plan says otherwise.
- Pain, injury, illness → no "train through it". If the user mentions pain, injury, or
illness, recommend rest and consultation with a qualified medical professional. For fever
or infection, give no training recommendation at all beyond rest.
- Progression limits. Keep recommendations inside ACWR 0.8–1.3 and avoid week-over-week
load increases greater than ~10% when ramping volume.
- Nutrition. Give only general, health-oriented nutrition guidance. Never provide calorie
targets, weight-loss plans, or extreme dietary advice; refer specifics to a sports
nutritionist.
- Decision support, not a substitute. All output is decision support — not a replacement
for a physician or a certified coach. The athlete's subjective feel/RPE overrides the
numbers; say so when numbers and reported feel disagree.
Reference files (progressive disclosure)
Load these on demand — do not read them all up front. Read each one at the moment listed:
| File | Read when |
|---|
references/data-sources-and-normalization.md | Always, before Step 4 (normalizing MCP data into the canonical schema); also when a source exposes unfamiliar field names or units. |
references/zones-and-thresholds.md | In Step 2 when validating or estimating thresholds (FTP, LTHR, threshold pace, CSS), and whenever you prescribe target zones or the user asks about zones. |
references/training-load-and-pmc.md | In Step 6 when interpreting CTL/ATL/TSB, ACWR, monotony/strain, ramp rate, or the load formulas behind per_activity.load_tss. |
references/recovery-and-readiness.md | In Step 6 when wellness data exists or any recovery flag (HRV_SUPPRESSED, RHR_ELEVATED, SLEEP_DEGRADED, COMBINED_RECOVERY_ALERT) fired. |
references/periodization-and-planning.md | In Step 8 when forming the next-session and block recommendation, and for race preparation/taper questions. |
references/glossary.md | When the user asks what a metric means, or you need a precise one-paragraph definition for the report. |
Workflow
Execute these eight steps in order.
Step 1 — Discover data sources
- Enumerate the connected MCP servers and their tools. Where a ToolSearch facility is
available, use it to find fitness-related tools.
- Match servers/tools against these name patterns (case-insensitive substring match):
garmin, strava, coros, polar, suunto, wahoo, whoop, oura, intervals,
trainingpeaks, fitbit. Also treat generically named tools as candidates when their
descriptions mention activities, workouts, wellness, HRV, or sleep.
- If no fitness source is found: tell the user no fitness MCP server is connected, briefly
explain how to connect one (add a Garmin/Strava/etc. MCP server to their Claude
configuration), and stop gracefully. Do not fabricate data or continue with an empty
analysis.
Step 2 — Establish athlete context
- Determine the goal race: date, distance (sprint / olympic / 70.3 / full), and priority.
Look in the sources first (calendar/race tools); if unknown, ask the user. If the user has
no race, note that and analyze for general fitness.
- Retrieve threshold values from the sources or the user: FTP (bike, watts), LTHR (bpm),
run threshold pace (per km), swim CSS (per 100 m), plus hr_max and hr_rest.
- Record which thresholds are missing and which are estimated rather than measured (this
drives
athlete.lthr_source / athlete.ftp_source and the data-gaps section of the
report). Read references/zones-and-thresholds.md before estimating anything.
Step 3 — Pull data
- Pull from every discovered source, not just the first one. The compute script
deduplicates cross-source double-counts; do not pre-filter.
- Activities: at least the last 42 days, all disciplines (swim, bike, run, strength, other);
pull 90 days if the source makes it cheap (few calls, no pagination pain).
- Wellness: the daily series for the same window — HRV (rMSSD), resting HR, sleep duration,
sleep score, recovery/readiness score, subjective fatigue, whatever exists.
- Athlete profile/thresholds, if the source exposes them.
- Keep raw responses available until normalization is done; never re-type numbers from memory.
Step 4 — Normalize into the canonical schema
Read references/data-sources-and-normalization.md for per-source field mappings and unit
conversions, then assemble one JSON object in exactly this canonical schema. All fields
are optional unless marked REQUIRED. If a value is unknown: omit it or use null — never guess.
Units are encoded in field names; dates are ISO 8601.
{
"athlete": {
"sex": "M | F | null",
"ftp_w": 250,
"lthr_bpm": 165,
"hr_max_bpm": 190,
"hr_rest_bpm": 45,
"threshold_pace_sec_per_km": 255,
"css_sec_per_100m": 95,
"lthr_source": "measured | estimated_from_hrmax | null",
"ftp_source": "measured | estimated | null"
},
"activities": [
{
"id": "garmin-123456",
"source": "garmin",
"sport": "swim | bike | run | strength | other",
"start_time": "2026-06-01T07:30:00Z",
"duration_sec": 3600,
"moving_sec": 3500,
"distance_m": 10000,
"avg_hr_bpm": 150,
"max_hr_bpm": 175,
"avg_power_w": 200,
"normalized_power_w": 210,
"avg_pace_sec_per_km": 300,
"avg_pace_sec_per_100m": 105,
"tss_reported": 55.0,
"rpe": 6,
"time_in_hr_zones_sec": [600, 1200, 900, 300, 0],
"time_in_power_zones_sec": [0, 0, 0, 0, 0, 0, 0],
"name": "Morning Run"
}
],
"wellness": [
{
"date": "2026-06-01",
"hrv_rmssd_ms": 65,
"resting_hr_bpm": 44,
"sleep_sec": 27000,
"sleep_score": 80,
"recovery_score": 75,
"subjective_fatigue": 3
}
],
"races": [
{ "date": "2026-08-02", "distance": "sprint | olympic | 70.3 | full", "priority": "A | B | C", "name": "..." }
]
}
Normalization rules:
id is REQUIRED, format <source>-<native id>; source, sport, start_time (UTC if
possible), and duration_sec (elapsed) are REQUIRED per activity. moving_sec falls back
to duration_sec when the source has no moving time.
time_in_hr_zones_sec is Z1..Z5; time_in_power_zones_sec is Z1..Z7 (Coggan) — only when
the source provides them; never reconstruct zone time yourself.
recovery_score normalizes Whoop recovery / Garmin Body Battery (morning value) /
Oura readiness to 0–100.
- Run pace and swim pace may be derived from distance and moving time when the source omits
them; everything else must come straight from the source.
- Maintain a running gaps list of every metric a source could not provide (no power meter,
no HRV, no sleep, missing thresholds, ...). This list feeds the report's
"Data coverage & gaps" section.
Step 5 — Compute metrics with the script
-
Write the canonical JSON object to a temporary file (e.g. triathlon_input.json in the
working directory or a temp directory).
-
Run:
python ${CLAUDE_SKILL_DIR}/scripts/compute_metrics.py INPUT.json [--today YYYY-MM-DD]
In Claude Code, ${CLAUDE_SKILL_DIR} expands to this skill's directory; on surfaces
without that substitution, resolve the directory containing this SKILL.md and run
scripts/compute_metrics.py from there. Omit --today to default to the latest date
present in the data; pass it explicitly if the user anchors the analysis to a specific day.
For a quick sanity check of the script itself, scripts/example_input.json is a small
synthetic fixture.
-
The script prints pretty JSON with these top-level keys: meta, per_activity, daily,
pmc, weekly, intensity, acwr, monotony, recovery, ramp, flags.
-
Use ONLY the script's numbers for CTL/ATL/TSB, ACWR, monotony/strain, ramp rate,
per-activity load, and weekly aggregates. Never compute or "adjust" these by mental
arithmetic, and never substitute your own estimates for the script's output.
-
Check meta.dropped_duplicates (cross-source dedup) and meta.data_gaps; both must be
surfaced in the report. If the script errors, fix the input JSON (schema mismatch is the
usual cause) and rerun — do not hand-compute the metrics instead.
Step 6 — Analyze
Interpret the script output, reading references as needed:
- Fitness/fatigue/form:
pmc (CTL/ATL/TSB) and ramp — see
references/training-load-and-pmc.md for interpretation bands and caveats.
- Load safety:
acwr (prefer the ewma ratio for flagging; report both), monotony,
and the flags array.
- Intensity distribution:
intensity (80/20 check — target roughly ≥75–80% of classified
time low intensity; mind method_note, since session-average classification underestimates
the high-intensity share of interval sessions).
- Recovery:
recovery (HRV, RHR, sleep statuses) — see
references/recovery-and-readiness.md when wellness data exists or recovery flags fired.
- Discipline balance:
weekly.per_sport versus the goal race distance.
Treat every flag in flags as something the report must mention (severity info may be a
single sentence; warn and alert need explanation and a consequence for the recommendation).
Step 7 — Report
Render the report using the template below, in the user's conversation language (translate
the section headings too). State data gaps explicitly. Keep it sober and technical: numbers
with units, trends with direction, no hype.
Step 8 — Recommend
Read references/periodization-and-planning.md, then:
- Recommend the next session: discipline, session type, duration, target zone, and a
1–2 sentence rationale tied to specific script numbers (e.g. TSB, recovery status,
discipline imbalance).
- Give block guidance: where the athlete sits relative to the race (base/build/peak/taper
or maintenance if no race), and what to adjust in the coming 1–2 weeks.
- Apply the hard safety rules last — they override the plan, the periodization logic, and
the race calendar.
Graceful degradation (missing data)
Iron rule: name every gap, and never invent numbers. Every metric you could not obtain,
every estimated threshold, and every fallback method used must appear in the
"Data coverage & gaps" section. A smaller honest report beats a complete-looking fabricated one.
- No power data (bike): the script falls back automatically along the load-method chain
(reported TSS → NP-based → avg-power → pace-based → HR-based → TRIMP → session-RPE → none).
Report which method carried most of the load (see
per_activity.load_method) and its
coarseness. Do not pretend power-grade precision.
- Missing thresholds (FTP / LTHR / run threshold pace / CSS): ask the user once. If still
missing, the script estimates LTHR ≈ 0.90 × hr_max where possible (always flagged); loads
fall back to HR/TRIMP/sRPE methods. Mark all affected numbers as estimates and recommend a
field test (see
references/zones-and-thresholds.md).
- No HRV: the HRV status will be
no_data. Base recovery assessment on RHR, sleep, and
TSB instead, and say that HRV-based readiness was not assessable.
- No wellness data at all: skip recovery-trend claims entirely; rely on TSB, monotony,
and subjective feedback from the user. The script raises
DATA_SPARSE — surface it.
- Short history (< 42 days): CTL is unreliable (
CTL_UNRELIABLE flag); say that fitness
trends need more history and keep recommendations conservative.
- Activities without any load signal (
load_method = none, load 0): list them in gaps;
do not assign them an invented load.
- No race on the calendar: analyze for general fitness; block guidance becomes a
maintenance/consistency recommendation rather than race-phase positioning.
Report template
Use this skeleton. Replace placeholders; translate headings into the user's language. Omit
the Flags section entirely when there are no flags.
# Training Report — {date range} ({n} days)
## 1. Snapshot
{One sentence: fitness CTL {x}, fatigue ATL {y}, form TSB {z}, recovery status {ok/watch/alert/no_data}.}
## 2. Discipline breakdown
| Discipline | Sessions | Time | Distance | Load (TSS) | Trend |
|---|---|---|---|---|---|
| Swim | {n} | {h:mm} | {km} | {tss} | {↑/→/↓} |
| Bike | {n} | {h:mm} | {km} | {tss} | {↑/→/↓} |
| Run | {n} | {h:mm} | {km} | {tss} | {↑/→/↓} |
| Other | {n} | {h:mm} | — | {tss} | {↑/→/↓} |
{1–3 sentences: intensity distribution ({low}% low / {mod}% moderate / {high}% high vs ~80/20
target), discipline balance vs goal race.}
## 3. Load & recovery trends
- CTL {x} ({±y}/week ramp), ATL {x}, TSB {x} — {interpretation}.
- ACWR: rolling {x}, EWMA {x} — {band interpretation}.
- HRV: 7d {x} ms vs 28d baseline {y} ms — {status}. RHR: {x} vs {y} bpm — {status}.
Sleep: {h:mm}/night vs baseline {h:mm} — {status}.
## 4. Flags
- {SEVERITY} {CODE}: {plain-language explanation and consequence}.
## 5. Next session
**{Discipline} — {type}, {duration}, {target zone}.**
{1–2 sentence rationale citing the specific numbers that drive this choice.}
## 6. Block recommendation
{Phase position relative to {race} on {date} (or maintenance if no race); what to adjust over
the next 1–2 weeks: volume, intensity, recovery emphasis.}
## 7. Data coverage & gaps
- Sources: {list}. Window: {n} days, {n} activities ({n} after dedup; dropped: {ids or none}).
- Missing/estimated: {every gap — e.g. no power meter on bike, LTHR estimated from hr_max,
no HRV data, swim CSS unknown}.
---
*{Footer, one line: This is decision support, not medical advice; how you feel (RPE) overrides the numbers.}*
Footer rule: the one-line disclaimer is mandatory in every report and must convey: decision
support, not medical advice; the athlete's RPE/subjective feel overrides the numbers.