Joules
Joules helps cyclists review their training, plan upcoming weeks, adjust workouts around life events, log completed sessions, and keep race and coaching context current through ChatGPT.
- Integration type
- Plugin
- Verification status
- Not applicable
- Platform
- ChatGPT
- Primary Subcategory
- Endurance Sport Training Planners
- Secondary Subcategories
- None listed
- Brand
- Joules
- Access
- Account required
- First tracked
- 2026-06-30
- Tool count
- 23
- Geography
- US
The Primary Subcategory used for this profile’s headline score.
Other Subcategories where the Integration is visible.
ChatGPT Plugin Discoverability Score
ChatGPT organic discovery is not live yet
Joules is tracked in the ChatGPT Plugin registry. Public organic-discovery measurement is not live for ChatGPT yet, so there is no score to publish today.
Get notified when your score goes live
Enter your work email and we’ll notify you when ChatGPT Plugin organic discovery scoring launches.
No spam. Unsubscribe any time.
Competing in ChatGPT Endurance Sport Training Planners
View CategoryHow the Discoverability Score works
Organic discovery scoring for Joules on ChatGPT is not live yet. The score will use measured agent conversations when it launches.
Organic discovery scoring is pending. Your Plugin score will appear on this scale when measurement goes live.
FoundDiagnostic
Whether Claude found your Plugin in connector search. It must be Found before it can reach the picker, but the score counts picker appearances—not search results.
PickedMain score
How often your Plugin appeared in the picker, or Claude invoked it directly, across contested conversations. This percentage is the Discoverability Score; the headline number is rounded.
PositionedDiagnostic
What position your Plugin appeared in when it was shown in the picker. This shows prominence, but it does not affect the score.
23 tools agents can invoke
<usecase> Adds a single race event to the athlete's calendar. Use when the athlete mentions a race they have signed up for or want to put on the calendar — `addRace` lands the event by itself, no planning session required. Returns the new `eventId` so a follow-up `removeRace` can target it if the athlete changes their mind. </usecase> <instructions> `date` is the race day in YYYY-MM-DD; past dates are rejected (athletes can't sign up retroactively). `name` is the race title. `priority` is A, B, or C — match what the athlete says, defaulting to B if they don't specify. `estimatedLoad` (optional) is the expected race TSS — leave it unset rather than guessing. `targetTSB` (optional) is how fresh the athlete should arrive: A-race 10–15, B-race 5–10, C-race 0–5. Requires the `joules.write` OAuth scope and an active Joules subscription. </instructions>
addRace
<usecase> Builds and validates one week of training, persisting it as a committable proposal. Renders NO card — this is the loop tool: call it repeatedly to converge the week into tolerance, then call `presentWeek({ proposalId })` to surface the week-preview card the athlete accepts. Pass the full event list for Mon–Sun — omitted days are rest days. Each event is processed (solver for system workouts, custom workouts get their load calculated, outdoor rides take their target load, races take their estimated load), load is summed, and the draft is checked against the week's target load (±20%). Returns a `proposalId` plus the processed week, an `inTolerance` flag, and a day-by-day fitness projection. Re-building the same week replaces the prior pending draft. </usecase> <instructions> `weekStart` is the Monday YYYY-MM-DD returned by `getPlanningContext` (also accepts any date inside the desired ISO week — snapped to Monday). `events` is an array of PlannedEvent objects — each with a `day` (monday…sunday) and exactly one of `system_workout` / `custom_workout` / `outdoor_ride` / `race` (or none for a rest day). Events placed on `lockedDays` from the planning-context payload are rejected. Scheduled races (in `scheduledRaces`) are NOT locked: you can place a workout the same day. `narrative` is required — the athlete-facing message describing the week, frozen now because the card's Accept button commits without another model turn. Plain text; open with what the week is about, walk through key sessions, close with what to watch for. Pass `targetLoadOverride` to pin a deliberately lighter or heavier week (time crunch, deload, life) — the week is then checked against that number instead of the planned target, and committing records it as this week's target. Omit it to plan to the planned target. Get the draft in tolerance, then call `presentWeek` to show it; if it lands out of tolerance, adjust the events and call `buildWeek` again. </instructions>
buildWeek
<usecase> Commits a week built by `buildWeek` — writes the processed events onto the athlete's calendar and stamps the week's narrative. Takes only the `proposalId`; the committable draft already lives server-side. Re-validates the frozen draft against live state first: if the target moved or a day got locked since the proposal was made, the commit is refused and you're sent back to `buildWeek`. Committing the same proposal twice is a no-op. </usecase> <instructions> `proposalId` is the `wp_…` token returned by `buildWeek`. The athlete usually commits via the Accept button in the week-preview card; call this only when committing on their behalf after they approve in chat. </instructions>
commitWeek
<usecase> Designs the athlete's training plan — the forward trajectory (blocks + weeks with intent and target TSS) plus the program math (progression rates, baseline load) and schedule (`availableDays`). One tool with a preview/commit pattern: `preview: true` returns materialized weekly TSS per block and a projected CTL/ATL/TSB curve through the window (distributed across the athlete's `availableDays` when set, uniform otherwise) without writing; `preview: false` commits the program patch and the trajectory in one atomic batch. Use preview to iterate on block shape before committing, then commit when the athlete approves. </usecase> <instructions> `blocks` is required — each block has a phase (base/build/peak/race/recovery) and a list of weeks with an intent (base/build/recovery/peak/taper). Target TSS is computed from the program's progression rates and (for taper weeks) the targetTSB of races on the calendar; pass `targetTssOverride` on a week to pin a specific value. `effectiveFrom` is optional — pass YYYY-MM-DD inside the desired ISO week (snaps to Monday) or omit for the upcoming planning week. Program math fields (`initialWeeklyLoad`, the four rate fields, `availableDays`) are optional patches — only the fields you pass are written. First commit auto-creates the program row with SQL defaults for unspecified fields; `programSnapshot.source` indicates whether the math came from the persisted row (`"program"`) or defaults (`"default"`). `preview` defaults to false. Requires the `joules.write` OAuth scope and an active Joules subscription. </instructions>
setPlan
<usecase> Search/identify specific activities and pick one to drill into. This is a SHORT-LIST tool — recent rides, filtered drill-downs, "find that race" lookups. Returns up to ~25 activity rows with the fields needed to identify a ride (id, name, date, sport, duration, load, NP, avgHr) so the model can either describe a single ride or pass an `id` to `getActivityDetail` for streams/laps/equipment. **Not for time-aggregated questions** like "how much did I train this month" or "how has my volume trended" — call `getFitnessHistory` (CTL/ATL + per-day load) or `getWeekSummary` (one week's totals) for those. Returning all rides over a long window so you can sum them is the wrong shape; this tool caps results at 25. </usecase> <instructions> All inputs optional. **Date range** — pick one: `window` shorthand ("7d", "14d", "28d", "90d", "180d", "1y", "5y", "all"; default "28d") OR `startDate` + `endDate` for an explicit calendar range around a known event. Long windows are usually the wrong call here — if the user is asking about overall volume use `getFitnessHistory` instead. **Sport** — `sport` filters by activity type (common: "Ride", "VirtualRide", "GravelRide", "MountainBikeRide", "Run", "Swim"); when omitted, filters to the canonical "all ride" bucket (Ride, VirtualRide, EBikeRide, MountainBikeRide, GravelRide, EMountainBikeRide, Handcycle, Velomobile) so MTB/gravel/virtual surface alongside road. Pass `sport` to narrow further or look at non-cycling. Match is on `sport_type` then `activity_type`. **Filters** — `minDuration` (seconds, matched against moving time with elapsed fallback), `minLoad` (power-based training load), `nameContains` (case-insensitive substring; when it matches nothing, `data.nameSearchFallback` carries the same-window candidates so you can locate the ride by date even when the athlete used a personal nickname). **Order** — `orderBy`: "recent" (default — newest first), "load_desc" (highest load first — top-N hardest), or "duration_desc" (longest first). Use load_desc / duration_desc with a longer window for "what was my hardest/longest ride this year" without dumping everything. **Limit** — `limit` defaults to 10, max 25. If the filter is too broad `summary.truncated` will be true; narrow the filter rather than raising the cap. Aggregates are intentionally not returned — see above. </instructions>
findActivities
<usecase> Returns the authenticated athlete's account state — current tier, trial days remaining if on trial, and subscription status if subscribed. Use this when the athlete asks about their trial, billing, or how to upgrade. </usecase> <instructions> Always available — works for trialing, pro, and expired users. `tier` is one of `trialing`, `pro`, or `expired`. `trial.endsAt` is unix seconds; `trial.daysRemaining` is 0 once the window has closed. `subscription` is populated when the athlete has paid (active, canceling, or past due). `reUpUrl` is a single-use authenticated link to app.joules.cc/account — present for `expired` users and `trialing` users with 9 or fewer days left. </instructions>
getAccountStatus
<usecase> Drill into one specific ride. Returns the canonical record (duration, load, NP, FTP at the time, average/max HR, kilojoules) **plus** stream- derived coaching summaries when streams have been processed: per-quarter splits (avg power/HR/cadence by quartile of moving time), time-in-zone (power and HR), Pw:Hr decoupling between halves of the ride, and the ride's climbs (each with distance, gradient, elevation gain, and time- weighted power/HR/cadence over the climb). Per-activity power curve (best avg watts at 5s, 1m, 5m, 20m, 60m, …) is included when efforts have been computed. The natural follow-up to `findActivities` whenever the user wants to talk about a single session ("how was that race", "did pacing fall off in the back half", "where was he in zone 4", "what did he do on the climbs"). Use it eagerly — this is the only tool that exposes per-ride streams analysis. </usecase> <instructions> Pass `id` — the canonical activity id (uuid) printed as the first field of every `findActivities` row. If you only have a date / sport / nickname, call `findActivities` first and copy the `id` from the row you want. Stream summaries depend on `streamsStatus === "completed"`; when status is "pending" / "skipped" / "failed", `summary.hasStreamData` is false and the quartile/zone/decoupling fields are null. Climbs are populated for outdoor rides with an altitude stream; indoor rides, flat rides, and rides whose streams are still pending or were skipped by tier policy (free-tier rides > 90 days old) all have `segments: []` — always read `streamsStatus` before concluding "no climbs" from the empty array. The ~360-sample downsampled preview arrays (watts/heartrate/altitude/cadence/distance, time-aligned via `streamSamples.timeOffsets`) ship in every response — the activity-detail widget renders them as a multi-series chart, and they're the ground truth for shape questions ("where did HR diverge from power?"). `decoupling.driftPercent` is one signal among many — read it with the climbs and quartiles, not on its own. </instructions>
getActivityDetail
<usecase> Returns a per-activity time series for one of the computed/stored metrics that `getActivityDetail` surfaces — but across many rides at once. Use it when the user asks how a per-ride metric has *trended* across the season ("how has my durability/decoupling trended in long rides", "is my NP holding up on the same loops", "is intensity factor creeping up on Z2 days", "are my average HR numbers drifting") or wants to *compare* buckets of rides ("is my durability worse on MTB than road", "are gravel days harder than tarmac"). Supports stored metrics (normalized_power, intensity_factor, kilojoules, load, hr_load, average_heartrate, max_heartrate, moving_time, distance_meters) and the stream-derived Pw:Hr decoupling (a durability proxy — first-half vs second-half aerobic drift). Pair with `renderChart` when the user wants to see the trajectory; narrate from the points directly otherwise. </usecase> <instructions> Pass `metric` (one of normalized_power, intensity_factor, kilojoules, load, hr_load, average_heartrate, max_heartrate, moving_time, distance_meters, decoupling) plus a date range — either `window` shorthand ("28d", "90d", "1y", etc.; default "90d") or `startDate` + `endDate` for an explicit calendar range. Filters: - `sport` — string or array. Pass one sport ("MountainBikeRide") to scope to a single discipline, or an array (["Ride","VirtualRide", "MountainBikeRide","GravelRide"]) to scope to several at once. Omit to scope to the canonical "all ride" bucket. Match is on sport_type with activity_type fallback. - `minDuration` (seconds, against moving time with elapsed fallback) screens out commutes / coffee spins so the trend isn't dragged by short noise. - `minLoad` — minimum power-based load. Use when the question is about hard rides specifically. Comparison shape: - `summary.bySport` is always returned — an array of per-sport_type aggregates ({ count, dropped, min, max, mean, last }) over the same series. For road-vs-MTB ("is durability worse on MTB"), pass `sport: ["Ride","VirtualRide","MountainBikeRide","GravelRide"]` and read the per-bucket mean/last off `summary.bySport` — no need to re-aggregate the points yourself. The model maps the raw sport_type buckets to "road" vs "MTB" in its read. Results are chronological (oldest first) with each point carrying { date, value, activityId, activityName, sportType, durationSeconds }. Activities that don't have the requested metric populated (e.g. `decoupling` for a ride with no streams yet, or `normalized_power` on a Run) are dropped from the series — `summary.dropped` reports how many. Capped at 250 activities per call — `summary.truncated` is true when more rides matched; narrow the filter rather than raising the cap. This tool is NOT for "today's CTL" or "this week's total load" — call `getFitnessHistory` (day-level CTL/ATL/TSB) or `getWeekSummary` for those. </instructions>
getActivityMetricsOverTime
<usecase> Returns the athlete's aerobic efficiency (EF = watts per heartbeat on Z2 efforts) as a per-ride time series with a coach-facing summary. Use this when the user asks "is my base coming around", "is my Z2 work paying off", "is my aerobic engine improving", or "should I keep doing easy rides". A rising EF trend across weeks is the cleanest signal that Z2 base training is producing real cardiovascular adaptation — the athlete is making more power for the same heart rate, or holding the same power on a lower heart rate. EF is only computed for rides with both power and HR streams and at least 20 minutes inside Z2 by power; sprints, all-out intervals, HR-only rides, and rides without an FTP snapshot are excluded. </usecase> <instructions> Pass an optional `window` shorthand ("28d", "90d", "180d", "1y"; default "90d") or an explicit calendar range via `startDate` + `endDate` (mutually exclusive with `window`). `sport` filters by canonical activity type — string or array. Omit to scope to the "all ride" bucket. `data.points` is chronological (oldest first), capped at 250 entries; each carries { date, value, activityId, activityName, durationSeconds }. `summary.trend` is one of "improving" | "declining" | "stable" | "insufficient_data" — read this rather than re-deriving from the points. `summary.pctChange` compares the mean of the first 20% of points (baseline) against the mean of the last 20% (recent), expressed as a percentage (e.g. `-6.8` means recent is 6.8% below baseline); null when fewer than 4 points are available. `summary.trendSlope` is the linear-regression slope in W/bpm per week. Athletes with no power meter, or windows with no qualifying Z2 efforts, return an empty `points` array and `trend: "insufficient_data"` — say that plainly rather than guessing. </instructions>
getAerobicEfficiency
<usecase> Returns the athlete's profile: first name, FTP, weight, units, country, timezone, total activities by sport, available date range, and whether training-load data exists. Useful for grounding responses in the athlete's specifics — concrete watts instead of "% FTP", their name instead of "the athlete", their primary sport when filtering activities. </usecase> <instructions> `profile.activityCounts` is sorted descending; index 0 is the primary sport and is the natural value for `findActivities`'s `sport` filter. `summary.hasActivities` indicates whether `findActivities` has data; `summary.hasFitnessData` indicates whether `getFitnessHistory` has a series. </instructions>
getProfile
<usecase> Returns the athlete's CTL (fitness), ATL (fatigue), TSB (form), and daily load as a time series across a window — one point per calendar day. Use this whenever the user asks about a trend ("how has my fitness changed", "am I building or losing fitness", "what's my fitness trajectory"). The summary includes start/end CTL, peak CTL, and the net change so you can cite real deltas instead of inferring them from the latest snapshot. When called with `endDate` alone, the window is anchored to that date instead of today — used to reconstruct fitness state going into a past event. Pass `startDate` + `endDate` together for a precise calendar range (training blocks, seasons). Optionally request a comparison series in the same call to answer year-over-year or block-over-block questions. </usecase> <instructions> Range inputs (pick one style): omit both for a rolling window ending today; pass `endDate` alone with `window` to anchor a rolling window's end to a past date (use the activity's startDate from `findActivities` to answer "what was my fitness going into <event>" — pair with `eventLabel` for an event marker); pass `startDate` + `endDate` together for an explicit calendar range (do not also pass `window`). Comparison inputs (pick at most one): `comparisonStartDate` + `comparisonEndDate` for an explicit comparison range; `comparisonMode: "same_dates_previous_year"` for the identical calendar dates one year earlier; `comparisonWindow: "previous_<28d|90d|180d|1y>"` for the block of that length immediately preceding the current range. When a comparison is requested, `data.comparison` holds its window + points and `summary` carries the comparison-aware deltas (`comparisonCtlDelta`, `ctlDeltaVsComparison`, `comparisonTotalLoad`, `totalLoadDeltaVsComparison`). For any anchored response (a call with `endDate`), `summary.eventSnapshot` reports both `preEvent` (end-of-day stats from the day BEFORE the anchor — the true going-in state) and `postEvent` (end-of-day stats ON the anchor day, including that day's load). Use `preEvent` when reasoning about how the athlete entered the day. Days with no training-load data are omitted (no zero-padding). If `summary.pointCount` is 0 the athlete has no fitness history in the current range — say so and stop. </instructions>
getFitnessHistory
<usecase> Opens a weekly-planning conversation. Returns the planning system prompt, the previous week's actuals for opening the conversation, the target week's bounds and TSS target, which days are off-limits because they're already completed or time-off, scheduled races to plan around, and the season-block context. Read-only — no server-side session is created; the conversation itself carries the draft state until you call `buildWeek` to persist a committable proposal. </usecase> <instructions> `weekOf` is optional — pass a YYYY-MM-DD inside the desired ISO week and the tool snaps to Monday. Default is the upcoming planning week (Mon–Thu → this Monday, Fri–Sun → next Monday). Requires the `joules.write` OAuth scope and an active Joules subscription. Always start the conversation by reading the `previousWeek` field and acknowledging what the athlete actually did before asking what's different this week — do not skip that step. `buildWeek` takes `weekStart` as a parameter; you can pass the same `weekStart` returned here. </instructions>
getPlanningContext
<usecase> Returns the athlete's mean-max power curve — best average watts at standard durations from 5 seconds to 3 hours — over a window, plus the latest fitted CP/W' model. Optionally returns a second curve for any chosen comparison range so a single call can answer "how does my power profile compare to <this time last year | the prior block | a specific season>". Also supports the durability read: pass `afterKj` as an array of cumulative-work thresholds (any of 1000, 2000, 3000 alongside the default 0) to compare fresh and fatigued curves in a single call — "how does my 5-min power hold up after 2000 kJ of riding?". Use to answer "am I getting stronger", "where am I improving", or "how durable am I". </usecase> <instructions> Range inputs (pick one style): omit `startDate`/`endDate` for a rolling window via `window` ("28d", "90d" default, "180d", "1y", "all"); pass `startDate` + `endDate` together for an explicit calendar range (do not also pass `window`). Comparison inputs (pick at most one): `comparisonWindow` (legacy) for the immediately preceding block of that length; `comparisonMode` for a named helper — "previous_period" (same length as current, immediately preceding), "same_dates_previous_year" (identical calendar dates one year earlier), "same_calendar_month_previous_year" (the calendar months the current range overlaps, one year earlier), "custom_range" (requires `comparisonStartDate`+`comparisonEndDate`); or `comparisonStartDate`+`comparisonEndDate` directly. When a comparison is returned, both `current` and `comparison` carry their own `cp` (the most recent fitted CP/W' model at or before that range's end), and `summary.criticalPowerDeltaWatts` is current.cp − comparison.cp. `summary.pointCount` of 0 means no power-meter rides in the current range — say so directly. CP/W' fields are null when the athlete has no fitted model in that period. Pass `afterKj: [0, 2000]` (or `[0, 1000, 2000, 3000]` for the full spread) for durability: each entry of the array becomes one curve in `data.curves`, sharing the same date window. `data.current` mirrors `data.curves[0]` so existing readers continue to work. When the request includes threshold 0 alongside at least one fatigued threshold, `summary.durabilityDelta` reports retention at the anchor durations (5s/1min/5min/20min/60min). A low `pointCount` for a fatigued threshold means the athlete has done few rides past that much accumulated work. </instructions>
getPowerProfile
<usecase> Returns the athlete's rider phenotype — sprinter, puncheur, diesel, or all-rounder — together with strength/limiter flags for the five canonical duration buckets (sprint, anaerobic, VO2, threshold, durability). Use this to tailor reads and recommendations to the kind of cyclist the athlete actually is: a sprinter and a diesel rider need very different training emphases even at the same FTP. `summary.topStrengths` and `summary.topLimiters` are the quick-reference lists for prose answers; `data.buckets` carries the per-duration ratios behind the classifications. </usecase> <instructions> No inputs. Profile is derived from the athlete's current mean-max power curve over the last 90 days; falls back to all-time when 90 days is too sparse. `summary.hasEnoughData` is false when fewer than 3 standard durations have a best effort even after the fallback — in that case `data.phenotype.type` is null and the model should say so plainly. `data.phenotype.confidence` is "high"/"medium"/"low" (bucketed from the underlying 0–1 score, which itself discounts when anchor durations are missing). `summary.powerCurveWindow` echoes "90d" or "all" so the model knows which window the read used. For the underlying watts at each duration, follow up with `getPowerProfile`. </instructions>
getRiderProfile
<usecase> Returns the athlete's training program settings (name, notes, principles, available days) plus the live plan — blocks in calendar order, each with its phase, week range, coach notes, and the per-week intent + load target (TSS) that defines the ramp. The shared planContext (current week position, current block, racing season, taper flag) rides on every plan-tool response, so call `getPlan` only when you need the deeper view: full block structure, full ramp, program-level settings. For one week's prescribed training, call `getTrainingWeek` instead. </usecase> <instructions> No arguments. The plan is implied by the authenticated athlete. When `summary.hasPlan` is false, the athlete hasn't set one up yet — direct them to call `setPlan` (or use the planning-session flow). `data.plan.blocks[]` is ordered first-to-last; each block's `startWeek`/`endWeek` are 1-indexed inclusive bounds into the plan, and each week carries `weekStart` (Monday YYYY-MM-DD — the natural key under the new schema), `intent` (base/build/recovery/peak/taper), and `targetLoad` (TSS). Each block exposes `id` (e.g. `blk-42`); pass it to `updateBlock` to rename the block, change its phase, or refresh its notes. Per-week TSS edits go through `setPlan`. </instructions>
getPlan
<usecase> Returns one ISO week (Monday–Sunday) of *prescribed* training keyed by the week's Monday: each day's events (workouts, races, vacation/sick/ injured days) with planned load, intensity, focus, and the interval structure for each workout inline. The week is identified by `weekStart` (the natural key under the new training data model — the `(app_account_id, week_start)` pair on `training_weeks`). The shared planContext (current block, racing season, taper flag) rides on every response. Use whenever the athlete asks "what's on this week", "what's left", "walk me through Thursday's session", or to read a specific session before discussing whether to do it. </usecase> <instructions> `weekOf` is required and must be a YYYY-MM-DD inside the desired ISO week — the tool snaps to the Monday. Resolve "this week" / "last week" against the briefing's current-date context before calling. Any week overlapping the live plan is fair game (past, present, future); weeks outside the plan return `summary.hasPlanWeek: false` (the request is still served — the day-by-day events still appear, just without the plan-level fields). `includeNonWorkoutEvents` defaults to true so races and time-off days appear on the calendar — set false only when the athlete asked specifically about workouts. For the actual ride on a completed workout, follow up with `getActivityDetail` using `linkedActivity.activityId`. For aggregate completed-side comparison, call `getWeekSummary` for the same week. Numbers belong in charts — describe the week qualitatively in prose. </instructions>
getTrainingWeek
<usecase> Returns one ISO week (Monday–Sunday) of training: per-day load and ride count, weekly totals (load, moving time, rides, average IF), the start/end CTL/ATL/TSB snapshot, and the prior 4-week mean for comparison. Use whenever the user asks about "this week", "last week", or a specific week. </usecase> <instructions> `weekOf` is required and must be a YYYY-MM-DD inside the desired ISO week — the tool snaps to the Monday. Resolve "this week" / "last week" against the briefing's current-date context before calling. The `priorMean` block is computed from the 4 weeks immediately before the requested week; `summary.loadDeltaVsPriorMean` is null when no prior data exists. For drill-in on individual rides, follow up with `findActivities` filtered to the same date range. </instructions>
getWeekSummary
<usecase> Returns the athlete's time-in-zone distribution summed across all rides in a window — 7-zone power and 5-zone HR. Use this whenever the user asks how polarized their training has been, whether they're spending enough time in Z2, whether they're doing too much threshold work, or whether the distribution is appropriate for where they are in the training block. `polarizationRatio` is (Z1+Z2) / (Z5+Z6+Z7) by time on the power axis: a high ratio indicates polarized training, a low ratio indicates threshold-heavy training. Pair the read with the athlete's own training phase context — the appropriate distribution differs across base, build, and peak. </usecase> <instructions> Range inputs (pick one style): omit both for a rolling `window` ending today (default "90d"); pass `startDate` + `endDate` together for an explicit calendar range. Sport filter: pass `sport` (a single canonical activity type, e.g. "MountainBikeRide", or an array of them) to scope to specific disciplines. Omit `sport` to scope to the canonical "all ride" bucket — MTB / gravel / virtual surface alongside road. Power and HR aggregates are independent: a ride that has FTP-derived zones contributes to power even if it has no HR signal, and vice versa. `data.power` is null when fewer than 3 rides in the window carry power-zone data — `summary.hasEnoughData` reports the same flag. `data.hr` follows the same gate at 3 rides. `polarizationRatio` is null when the high-intensity bucket (Z5+Z6+Z7) is zero or when power data is sparse. `summary.dominantZone` is the highest-share power zone (or HR zone when power is unavailable). For drill into a single ride, follow up with `findActivities` filtered to the same window and `getActivityDetail` on the ride id. </instructions>
getZoneDistribution
<usecase> Records a stretch of time off (vacation, sick, or injured) on the athlete's calendar from `from` to `to` inclusive, and soft-deletes any non-completed workouts already scheduled inside that range — the calendar reflects the truth immediately. Use when the athlete reports they'll be away or unable to train, or when they're returning from a period off and want it logged retroactively. </usecase> <instructions> `from` and `to` are YYYY-MM-DD calendar dates (UTC); `to` must not be before `from`. Single-day off → pass the same date for both. `type` is one of `vacation`, `sick`, `injured`. `notes` is an optional short label shown on the calendar; defaults to the capitalized type. Idempotent on the same range — repeating the call returns the same `eventId` with `wipedWorkoutCount` of 0. Overlapping ranges of the same type are merged into the existing event rather than duplicated. Completed workouts are never touched. </instructions>
markTimeOff
<usecase> Surfaces a week built by `buildWeek` to the athlete as a week-preview card with an Accept button. Takes only the `proposalId`; the committable draft already lives server-side. Call this once the draft is in tolerance — it is the step that actually shows the athlete the plan. The card's Accept button commits directly, so no further model turn is needed. </usecase> <instructions> `proposalId` is the `wp_…` token returned by `buildWeek`. Present only a pending proposal; a superseded/expired/committed draft is refused — rebuild with `buildWeek` first if needed. </instructions>
presentWeek
<usecase> Removes a race from the athlete's calendar (soft-delete). Pair with `addRace` when an athlete cancels a signup or signed up to the wrong event. Refuses to delete completed races — those are part of training history. </usecase> <instructions> `eventId` is the value returned by `addRace`'s `data.eventId`, or the race row's id from any read tool. Only race events owned by the caller can be removed; workouts and time-off use other deletion paths. Requires the `joules.write` OAuth scope and an active Joules subscription. </instructions>
removeRace
<usecase> Renders a chart inline in the user's chat. Relevant when the user asks to see a chart, visualization, or trend — for example a fitness trajectory plot, weekly load shape, or year-over-year comparison they've requested. Pairs with the data tools, which return the underlying numbers for narration. </usecase> <instructions> Pass `metric` (one of ctl, atl, tsb, daily_tss, cumulative_tss, weekly_tss), `ranges` (1-4 items, each `{label, start, end}` with YYYY-MM-DD dates), and optional headline copy (`eyebrow`, `name`, `stats`). Chart type is decided by the metric: `daily_tss` and `weekly_tss` render as bars; the rest render as lines. `align` controls how multiple ranges overlay: - "absolute" (default for one range) — true calendar dates on x-axis. Use for sequential ranges that share a timeline ("last 90d" alone, or "last 90d" vs "prior 90d"). - "doy" — day-of-year overlay so Jan 1 of one year sits on top of Jan 1 of another. Use for year-over-year comparisons. Headline copy guidance — keep all three short, the model narrates the rest. Always use plain terms, never acronyms: "Fitness" (not CTL), "Fatigue" (not ATL), "Form" (not TSB), "Load". - `eyebrow` — small context line, e.g. "Last 90 days · Fitness", "Apr 27 – May 3". - `name` — qualitative phrase, e.g. "Productive build", "Ahead of last year". - `stats` — one anchored number you'll cite in chat, e.g. "Now 67 · +22 since Jan 29". `yLabel` is optional — defaults to a sensible plain label per metric (e.g. "Fitness", "Load"). Override only when the default is wrong for the question. </instructions>
renderChart
<usecase> Patches metadata on a single block in the athlete's live trajectory — `name`, `phase`, and/or `notes`. Use when `getPlan` surfaces a block whose label or notes need refreshing (e.g. the athlete's goal shifted, or the coach's framing for a phase needs an edit). Per-week TSS edits and structural rewrites — moving block bounds, adding/removing weeks, changing a week's intent — go through `setPlan` instead. </usecase> <instructions> **blockId** — the `id` returned for the block in `getPlan`'s `data.plan.blocks[].id` (format `blk-<integer>`). **name**, **phase**, **notes** — supply only the fields you want to change. Pass an empty string for `notes` to clear it. `phase` must be one of base/build/peak/race/recovery. At least one field is required. </instructions>
updateBlock
How do I improve a ChatGPT Plugin's discoverability?
The levers are the listing surface agents actually read: names, descriptions, keywords, tool metadata, and registry health. Which lever matters depends on where discovery breaks, which is what continuous measurement shows.
What are Joules alternatives on ChatGPT?
As of 2026-08-14, Joules competes with AI Endurance, COROS, Endurance Planner, Fitness AI Connector, Flow State, Freediver, Leo - Running Coach, rit.run, Tredict in ChatGPT Endurance Sport Training Planners, ranked by public Discoverability Score.
Where is this profile measured?
This profile uses the geography attached to the latest public registry snapshot: US. Locale tags are intentionally omitted.