Created by
Vekta
Last updated
Categories
Race
Built tool
Made for
Coaches
Athletes
Runs
On demand
Topics
Power profiling
Race pacing
It reads the course of a planned event and the athlete's best efforts from the last 8 weeks at every level of fatigue, and sets each climb's target from the fatigue they will carry into it. Each target is then adjusted for the climb's altitude, using published research.
Sliders change the plan: effort, weight, bike and whether the athlete is acclimatised to altitude. Every target recalculates as they move. It is a plan built from the athlete's own data, and the pacing decision stays with the athlete and coach.
How to run it
1
Paste the prompt
Open Claude Code, Codex or Gemini CLI and paste it. Fill in anything in square brackets.
2
Or install the file
Unzip it into ~/.claude/skills for Claude Code, or ~/.agents/skills for Codex and Gemini CLI. Then run it as a Skill by name each time.
3
Let it run
A scheduled skill runs on your own computer, so it has to be on. Everything it reads is your data, through your key.
What it touches
Reads from Vekta
Best efforts at every level of fatigue, streams, CP, W′, weight, sleep, the planned event
Writes to Vekta
Nothing. It only reads.
Works in
Claude Code, Codex, Gemini CLI
GET THIS SKILL
The prompt, and the file.
Unlocked on this browser for a year. Paste the prompt into Claude Code, Codex or Gemini CLI, or install the file and run it by name.
THE PROMPT AND FILE
Race Day Simulator Build a race day plan for [race] on [date]. If the race or the date is missing, ask for both before doing anything else. Who it is for, and what it produces An athlete on Vekta, planning a race. A coach can run it too, for one athlete at a time. The result is one interactive HTML page: a single file, no build step, opened in a browser. Every climb target comes from the athlete's own best efforts in the last 8 weeks, at the fatigue they will carry into that climb, read from the Vekta API, then adjusted for the climb's altitude. You are the analyst. Use Vekta's data as it comes and never build a performance model of your own: no curve fitting, no extrapolation past the data and no thresholds of your own. The only additions are the altitude adjustment and the speed physics below, and the page says where each comes from. The page speaks to the athlete as "you", also when a coach runs it, so it can be shared as it is. Inputs - [race]: the event name. [date]: the day it is ridden. - The athlete. An athlete's own key uses the userId `me`. A coach's key lists the athletes it coaches at GET /users (id, firstName, lastName). Ask which athlete, then use that id in every call below. - The Vekta API key. Read it from the environment variable VEKTA_API_KEY and send it as `Authorization: Bearer <key>`. If it is not set, ask for it. Never print it, never log it, never write it into the page. - Base URL https://api.joinvekta.com. Every list endpoint returns a JSON array. Date filters are `from` and `to`, inclusive, YYYY-MM-DD. There is no pagination. - The athlete's weight, in kg. Read it from Vekta (step 1 below). If Vekta holds none, ask for it before building the page. Never use a default. - The course: a GPX of the route if the athlete has one, otherwise the organiser's published route and climb profiles. - Whether the athlete will be acclimatised to altitude, meaning several weeks at altitude before the race. Default: not acclimatised. What to read, and over what window The window is the 8 weeks (56 days) ending today, the day the plan is prepared, so every target reflects current fitness. Never widen it with older rides: fitness months ago is not fitness now. Pull the data with a script, compute everything below, and bake the results into the page as constants. The page never calls the API and never contains the key. 1. GET /users/{userId} for firstName, lastName and latestMetrics. The weight is the latestMetrics entry of type weight; if there is none, the latest weight row from step 5; if neither exists, ask (see Inputs). 2. GET /users/{userId}/activities?from=<today minus 83 days>&to=<today>. Per activity: id, startedAtLocal, sport, subSport, trainingStimulus, volume (Vekta's training load score), intensity (0 to 100), effectiveDuration (s), work (kJ). Leave out activities with no startedAtLocal. Every sport counts for load, days trained and the stimulus chip. The rides are the activities with sport cycling dated inside the 56-day window; where two share the same start minute, duration and work, keep one. 3. GET /activities/{activityId}/best-efforts for every ride, one call each. Keep the entries of power[] that carry afterWorkPerKg; a ride with none (a manual entry, or no power meter) is skipped. Write every response to a local cache file keyed by activity id, and on a rerun call only the rides not in it. The cache holds the athlete's data: keep it private and out of the page. 4. GET /activities/{activityId}/streams for every outdoor ride (subSport not indoorCycling or virtualActivity), one call each, cached the same way. Use speed (m/s), grade (%), power (W) and altitude (m) for two things only. Flat-road power: the mean power over every moving second (speed above 2 m/s) with grade between -1% and +1%, with the hours it covers. Home altitude: the median of every altitude sample. If no outdoor ride has streams, ask for the athlete's usual flat-road power and the altitude they train at. 5. GET /users/{userId}/metrics over the window. Rows are dated, one per type: critical power (W), w prime, max power (W), vo2max, weight (kg). For critical power, w prime and max power take sport cycling. Take the latest row of each type, and for critical power also the latest row dated on or before today minus 28 days. W′ arrives in J or kJ, per the row's unit; show it in kJ. CP and W′ are shown for context. No target is derived from them. 6. GET /users/{userId}/sleeps?from=<today minus 28 days>&to=<today>. Per night: day (the wake-up date), averageHrv (ms), restingHeartRate (bpm). 7. GET /users/{userId}/events?from=<today>&to=[date], to confirm the planned event: name, date and local start time. The start time sets the start clock; if there is none, start at 07:00. The course Vekta does not hold the route. From the GPX, or else from the organiser's published route and the published profile of each climb, enter the course as an ordered list of the segments a rider would name: the approach, each categorised climb, each descent, and each valley or flat road between them. A long stretch between two climbs that both falls and rises is two segments, its descent and its valley road: a valley that climbs gently is ridden, not freewheeled. Per segment: name, type (flat, climb or descent), length in km, average gradient in %, start and end altitude in m, and for climbs the category (HC, 1, 2, 3 or 4) where one is published, never invented. Length × gradient must agree with the altitude change. Total distance is the sum of the lengths. The header shows the organiser's published climbing where there is one, because it counts the undulations the segments average out. The best-efforts surface, where every target comes from Each entry in power[] is the best average power (maxAveragePower, W) the ride held for one duration (s), counted only over windows that start after at least afterWorkPerKg kJ/kg of work was done. Level 0 is fresh; the deeper levels are power under fatigue. Use the levels and durations that come back, and never assume a set. (On a real account they are 0, 10, 20, 30, 40 and 50 kJ/kg across 38 durations from 1 s to 3 h.) The surface is the best across all rides in the window for each (level, duration): the highest maxAveragePower, in whole watts, with the local date of the ride that set it. A (level, duration) that no ride reached stays empty: never fill it. If the surface is empty, stop and say so before building. Reading a target for a climb (the lookup) Two numbers pick the target: d, the climb's predicted duration in seconds, and f, the work done from the start of the day to the foot of the climb, in kJ, divided by the rider weight slider. 1. At one level, read d by log interpolation between the nearest durations either side that have a value at that level: t = (ln d - ln d1) ÷ (ln d2 - ln d1), watts = w1 + t × (w2 - w1). A duration on record is read as it is. A level with no duration as long as d cannot be read at d. 2. Read the levels either side of f, each at d, and interpolate linearly: t = (f - L1) ÷ (L2 - L1). When f sits exactly on a level, read that level alone. 3. If either level cannot be read at d, read the deepest level below f that can. Past the deepest level the API reports, read the deepest level, or the deepest that can be read at d. 4. If no level holds an effort as long as d, read the longest fresh effort. Never read a shorter effort as the target for a longer climb, and never extrapolate past the data. Show no warnings: the climb's source line names what was read, as "from your best efforts between 20 and 30 kJ/kg", "from your best efforts at 40 kJ/kg" or "from your longest effort (2h 00m)". Altitude This adjustment comes from published research, not from Vekta, and the page says so. Aerobic power falls with altitude. Bassett et al. (1999), Medicine & Science in Sports & Exercise 31, 1665-1676, give the percentage of sea-level aerobic power at altitude x, in km: - not acclimatised (1 to 7 days at altitude): 0.178x³ - 1.43x² - 4.07x + 100 - acclimatised (several weeks at altitude): -1.12x² - 1.90x + 99.9 The athlete's efforts were ridden at their home altitude h (step 4). At altitude a, the factor is the curve's value at a divided by its value at h, capped at 1, so a climb below home altitude gains nothing. The curve follows the acclimatised setting. Speed from power Solve speed v (m/s) by bisection between 0.3 and 35 m/s, 80 iterations: the largest v such that m·g·sin θ·v + Crr·m·g·cos θ·v + 0.5·ρ·CdA·v³ < P × 0.976 with θ = atan(gradient/100), g = 9.81, Crr = 0.005, CdA = 0.32, m = rider weight + bike and kit (both sliders), ρ = 1.225·exp(-altitude/8435) at the mid altitude of the piece or segment, and 0.976 for drivetrain efficiency. These are standard values, not the athlete's, and the footer says so. The plan, segment by segment, in course order Keep a running total of work in kJ (cum) and of seconds (total). Both start at zero. - Climb. Effort% is that climb's slider (75 to 100). Split the climb into 12 equal lengths, each at its own mid altitude. Start from a guess of 1,800 s. Repeat up to 40 times: read the surface at (the guess, cum ÷ rider weight); ride each piece at those watts × effort% × that piece's altitude factor, solving its speed; the new duration is the sum of the pieces; if it is within 1 s of the guess, stop, otherwise set the guess to the mean of the two and repeat. The target is the time-weighted mean power of the pieces, in whole watts, and that is what the table shows. Its loss for altitude is 1 minus that mean ÷ the surface watts × effort%, as a whole %. Source line: "Climb N · X kJ/kg in your legs · from your best efforts between A and B kJ/kg · p% less for altitude (a0 to a1 m)". - Flat or valley road: ridden at the athlete's flat-road power (step 4), with the speed solved for the segment's gradient at its mid altitude. The effort column reads "your flat-road power". No target. - Descent: split into 12 equal lengths. Each piece freewheels (P = 0) at the speed the gradient gives; where that is slower than the athlete's flat-road power would carry them on level ground, ride that piece at flat-road power instead. The effort column reads "freewheeling", or "pedalling" when any piece is pedalled. No target. After every segment: kJ/kg at the foot = cum ÷ rider weight, taken before the segment is added; then cum += the segment's work in kJ, total += its duration. Totals - Moving time = total, as "Xh XXm", rounding to whole minutes before splitting hours and minutes. Sub-line: "finishes HH:MM from a HH:MM start, no stops". - Average power = cum × 1000 ÷ total, whole watts. Sub-line: "whole day, moving". - Climbing average = Σ(P × duration) ÷ Σ duration over the climbs, whole watts. Sub-line: "Xh XXm of climbing". - Total work = cum, whole kJ with a thousands separator. Sub-line: cum ÷ rider weight, 1 dp, "kJ/kg". - Time climbing = Σ climb durations, "Xh XXm". Sub-line: round(climb time ÷ total × 100), "% of the day". - Table footer: Total | total km, whole | blank | cum ÷ rider weight, 1 dp, "kJ/kg" | blank | the plain mean of the climb targets, whole watts, "N W avg" | blank | total as "Xh XXm". The controls - Effort on every climb: 75 to 100, step 1, default 100, readout "100%". Moving it sets every climb's own slider to the same value. - Rider weight: 55 to 90 kg, step 0.5, default the athlete's weight, readout to 1 dp, "66.0 kg". (Widen the range only if the weight sits outside it.) It changes the mass in the speed solver and every kJ/kg figure, so a climb can read a different level. It never changes the watts on the surface. - Bike and kit: 6 to 12 kg, step 0.1, default 8.0, readout "8.0 kg". - A tick box, "Acclimatised: several weeks at altitude before the race", default as given in Inputs. It switches the altitude curve. - One slider per climb in the table row: 75 to 100, step 1, caption "N% effort". It changes that climb only. Every control recalculates the whole day on input. Easing off one climb lowers cum, so the next climb reads a fresher level and its target moves. The page, in this order Page title: "Race Day Simulator". 1. Header. Eyebrow: "Race Day Simulator · prepared [today, as 25 May 2026]". H1: "[First name] [surname initial]. · [race]". Sub-line: "Your race day plan, built on Vekta's physiological model of you and personalised to your current fitness. Each climb's target matches the fatigue you will carry into it and the altitude you will ride it at." A second line: "[km] km, [climbing] m of climbing and [N, as a word] categorised climbs.", with "about" before the climbing when it is the organiser's figure. 2. "Condition today". One card: a grid of eight stats, each a label, a value and a line under it; then chips; then a note. Stats, in order: Load-8; Load-2; Load-2 : Load-8; Strain, 14d; Days trained, 28; Critical power; Deepest fatigue, 8 weeks; Hour power at [the deepest level holding an effort of an hour] kJ/kg. Load-8, Load-2, the ratio and Strain: the value today, then ▲ or ▼ and the whole-% change against the same figure a week ago, "on a week ago" (▬ 0% when it rounds to zero), always neutral. Days trained, 28: distinct local days with an activity, any sport, in the last 28 days, as "17/28". Critical power: the latest CP, with "[CP four weeks ago] W four weeks ago" under it. Deepest fatigue, 8 weeks: the deepest level on the surface, "[level] kJ/kg", with "the API's deepest level · [N] rides" under it when it is the API's top level, otherwise "[N] rides". Hour power: the surface at 3,600 s at that level, with "[the same at level 0] W fresh · last 8 weeks" under it. Chips: W′ (kJ, 1 dp); VO₂max (whole); Max power (W); Weight ("[kg] kg, from Vekta", or "entered by you"); 28-day stimulus (counts of trainingStimulus over the last 28 days, most frequent first, as "6 aerobic, 5 threshold, 2 recovery"). Then a short note in plain prose: Load-2 against Load-8 as the ratio, with Strain; days trained of the last 28; CP against four weeks ago ("the same as four weeks ago" when equal); "The targets come from your last 8 weeks only, so they show where you are now."; the kJ/kg this course puts in the legs at the foot of each climb, by name, at the default settings; last, HRV on the most recent night against its 28-night mean and resting heart rate against its 28-night mean, or, when no night is on record, "No sleep or HRV is recorded on this account." State what the record shows. No advice. Definitions. Load-8, Load-2 and Strain are computed the way the Vekta apps compute them, for the 28 days this panel shows: every series starts from zero 56 days before the first of those 28 days (today minus 83 days) and counts every dated activity, any sport, from that day on. One row per day: daily volume = the sum of the day's volume, 0 on a day with no session; daily intensity = the duration-weighted mean of the day's intensity (weights effectiveDuration), rounded to a whole number, 0 on a day with no session. Load-8 = the exponentially weighted moving average of daily volume, alpha 2/57. Load-2 = the same with alpha 2/15. Strain = the same average of daily intensity, alpha 2/15. No adjustment: the first value is the seed day's own total, and each next value is alpha × that day + (1 - alpha) × the day before. Today's values are shown; the week-ago values are the same computed for the 28 days ending 7 days earlier. 3. "The plan". Five KPI cards in a row: Moving time; Average power; Climbing average; Total work; Time climbing. Under them one line: "These targets are the best you have ridden in your last 8 weeks, at that length and that level of fatigue. Where you have not gone flat out for that long, there may be more in you: your critical power is [CP] W." Then, in a card, the elevation profile: an SVG 960 by 150, x = cumulative distance, y = altitude with the highest point near the top, the profile as a line in the text colour at 75% opacity, the area under it filled at 4.5%, each climb's span shaded in the accent yellow at 10% with its category centred above the summit in small muted capitals. Then the segment table: Segment | km | Gradient | kJ/kg at the foot | Effort | Target | Speed | Time. A climb row: the category chip, the name, the source line under it in small faint text, the effort slider with its caption, the target in bold as "263 W". Other rows are muted with the power not bold. Then the footer row. 4. "Adjust". The controls side by side, label above, readout right-aligned, then the tick box. Under them, with the first sentence up to the colon in bold: "Ease off on one climb and the whole day recalculates: you reach the next one fresher, and its target moves with you. Change your weight and every kJ/kg figure moves, because fatigue is counted per kilo." 5. "Where every target came from". The surface as a table: Effort length, then one column per level on record, "Fresh" for 0 and "after 10 kJ/kg" and so on. Seven rows: 5, 10, 20, 30, 45, 60 and 90 min (where one of those lengths did not come back, the nearest that did, labelled with its own length). Each cell: the watts in bold with "W", and under it in small faint text the date of the ride that set it, as "16 May"; a cell with no record shows an en dash in faint text. Under the table a note: "Your best average power for each length of effort, counted only from moments when at least that much work per kilo was already done, across [N] rides in your last 8 weeks ([window start] to [window end]), with the date of the ride that set each one. It comes straight from the API's best-efforts endpoint. A dash means no effort that long at that fatigue in 8 weeks: the page never fills it in. A climb that needs one is read at the deepest level that has one." Then a second note: "Altitude. This adjustment comes from published research, not from Vekta. Your efforts were ridden at a median altitude of [h] m, and aerobic power falls as you climb higher, so each climb's target is reduced along its length using Bassett et al. (1999), Medicine & Science in Sports & Exercise 31, 1665-1676: the curve for riders who are not acclimatised (1 to 7 days at altitude), or for acclimatised riders (several weeks at altitude) when you tick the box. At the top of [the highest climb] ([altitude] m) that is [p]% less power, or [q]% acclimatised." 6. Footer: "Read from /users/{id}/activities, /activities/{id}/best-efforts ([N] rides, [window]), /activities/{id}/streams ([M] outdoor rides, for your speed and power on flat roads and your home altitude) and /users/{id}/metrics. Course from the published [race] route and climb profiles. Target power comes from your recorded best efforts, reduced for altitude; speed is solved from that power against gravity, rolling resistance and air density at altitude, using standard values for drag, rolling resistance and bike, not yours. Flat and valley roads are ridden at your own flat-road power, [W] W (from [hours] hours of flat riding in 8 weeks), with the speed worked out from each road's gradient. Descents are freewheeling, at the speed the gradient gives; where a road is too shallow to freewheel faster than your flat pace, you ride it at your flat power. All times are moving time, with no stops." Add any timing rule the organiser publishes, for example a neutralised descent: "In the event itself, timing is frozen on the [name] descent, so your official time will be shorter." Other kinds of event - A time trial, or a flat race with no categorised climb: one target for the whole course from fresh, read at level 0 for its predicted duration with the same lookup rules, held through every segment, with the speed of each segment from its gradient. - A hill climb: one climb, from fresh. - Cyclocross, criteriums and other races of repeated surges: this simulator plans steady efforts. Say it is not built for them, and stop. Formats and units Dates as "25 May 2026"; the ride dates on the surface table as "16 May". Clock times 24-hour, "07:00". Durations "Xh XXm", minutes zero-padded; effort lengths on the surface table as "5 min". Distance: km to 1 dp in the table, whole in the header and the footer row. Gradient: 1 dp with "%". kJ/kg: 1 dp, except the climb source line and the levels, 0 dp. Power: whole watts, "W". Speed: km/h, 1 dp. Work: whole kJ with a thousands separator. Load-8, Load-2 and Strain: 1 dp. Percentages: whole. The ratio: 2 dp with "×". Weight: 1 dp on the slider. A missing value: an en dash. A unit follows a space, in a smaller muted size, except "%". British English throughout, and no em dashes. Design One self-contained HTML file. Plus Jakarta Sans from Google Fonts (400, 500, 600, 700). Body 14 px, line height 1.55, max width 1,180 px, page padding 32 px 20 px 72 px, antialiased. Cards: surface background, 1 px border, 10 px radius, 18 px 20 px padding. H1 26 px, weight 600. Section titles 14 px, uppercase, letter-spacing 0.06 em, muted, weight 600, 38 px above and 14 px below. KPI labels 11 px uppercase muted; KPI values 23 px, weight 600, tabular numerals, with the unit at 13 px muted; KPI sub-lines 12 px muted. The stat grid: 1 px gaps on the border colour, 8 px radius, stat labels 10.5 px uppercase muted, values 17 px weight 600, lines under them 11 px weight 600. Notes: surface-2 background, 1 px border, 8 px radius, 13 px muted text. Table headers 11 px uppercase, letter-spacing 0.05 em, muted, on surface-2; cells 11 px 13 px padding, 1 px rules; numeric columns right-aligned with tabular numerals; non-climb rows muted at 13 px. Sliders take the accent yellow. Category chips: pills, 11 px, weight 600; "HC" plain, any other category with a yellow border on the amber background. Slider captions and source lines 11 px faint. Missing values on the surface table: faint. Tokens, light: bg #F2F0EB, surface #FFFFFF, surface-2 #FAF9F6, border #E4E1D9, text #16191d, muted #6C7681, faint #A2A9B2, yellow #FBCE01, red #B23A2E, amber #8A5A06, amber-bg #FCF6E8, green #1C7346, green-bg #EDF4F0. Tokens, dark, under prefers-color-scheme: dark and whenever the root carries data-theme="dark": bg #0B0E13, surface #13171E, surface-2 #171C24, border #252C36, text #E9E7E1, muted #8F99A5, faint #646E7A, red #E88C7E, amber #D3A462, amber-bg #221B10, green #63BC92, green-bg #14241D. Yellow stays #FBCE01. Responsive: five KPI columns, three under 1,000 px, two under 760 px; the stat grid four columns, two under 760 px; the table cards scroll sideways on a phone. Numbers in the plan table never wrap; the source line under a climb wraps at about 60 characters. Rate limits 300 requests a minute per key, 30 a minute per athlete. A 429 carries Retry-After, in seconds: wait that long and retry, up to four attempts. The profile, activities, metrics, sleeps and events are five requests against the athlete's 30. Best efforts and streams are one request per ride each and count against the key only, so pause 0.25 seconds between those calls. With the caches, a rerun fetches only the rides it has not seen. This is a plan built from the athlete's own recorded efforts. It shows the best already done at each level of fatigue, adjusted for altitude. Decisions on the day stay with the athlete and the coach. Docs: api.joinvekta.com/openapi.json
Planning
Race training plan
A periodised plan for your target race, built from the course's demands and your power profile, and fitted to the hours you actually train.
Reports
Training Response Report
A progress report for every athlete you coach, over 7, 30 or 60 days, written for them: the verdict, the numbers and how the body responded.
Reports
Weekly training review
An automatic weekly review of your training against the month before, and shows what it means for next week.



