Triathlon coach
Skill HenrikBrehm/triathlon-coach-skill/skills/triathlon-coach
Triathlon coach skill for Claude Code: pulls swim/bike/run + recovery data from connected fitness MCP servers (Garmin, Strava, Coros, Whoop, Oura, ...), computes CTL/ATL/TSB, ACWR & 80/20 distribution deterministically, and recommends your next session - with hard safety guardrails.
npx -y skills add HenrikBrehm/triathlon-coach-skill --skill triathlon-coachAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
15.5 KB, as published. Nobody here has run it
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_ALERTflag, orACWR_HIGHat severityalert, 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_sourceand the data-gaps section of the report). Readreferences/zones-and-thresholds.mdbefore 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:
idis REQUIRED, format<source>-<native id>;source,sport,start_time(UTC if possible), andduration_sec(elapsed) are REQUIRED per activity.moving_secfalls back toduration_secwhen the source has no moving time.time_in_hr_zones_secis Z1..Z5;time_in_power_zones_secis Z1..Z7 (Coggan) — only when the source provides them; never reconstruct zone time yourself.recovery_scorenormalizes 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.jsonin 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 runscripts/compute_metrics.pyfrom there. Omit--todayto 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.jsonis 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) andmeta.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) andramp— seereferences/training-load-and-pmc.mdfor interpretation bands and caveats. - Load safety:
acwr(prefer theewmaratio for flagging; report both),monotony, and theflagsarray. - Intensity distribution:
intensity(80/20 check — target roughly ≥75–80% of classified time low intensity; mindmethod_note, since session-average classification underestimates the high-intensity share of interval sessions). - Recovery:
recovery(HRV, RHR, sleep statuses) — seereferences/recovery-and-readiness.mdwhen wellness data exists or recovery flags fired. - Discipline balance:
weekly.per_sportversus 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_UNRELIABLEflag); 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.