Every time an OpenClaw agent answers a question about your latest ride or run, a multi-step pipeline fires behind the scenes. An OAuth2 token is validated, an HTTP request hits one of Strava’s versioned API endpoints, a JSON payload containing GPS coordinates, heart rate samples, and power readings arrives, and Claude parses that payload into a human-readable response. None of this is magic. It is a deterministic sequence of API calls, data transformations, and language model interpretation that can be understood, debugged, and extended.
This article breaks down that pipeline. It covers the architectural layers of the OpenClaw Strava skill, the Strava API data model, token lifecycle management, rate limiting strategies, data processing logic, and advanced features like GPS stream analysis and training load estimation. The goal is to give developers, power users, and the technically curious a clear picture of what happens between “How was my run?” and the answer that appears in chat.
Architecture Overview
The strava-connect skill follows the standard OpenClaw skill architecture: a registration manifest, a set of tool definitions, and handler functions that execute when Claude invokes those tools.
At a high level, the runtime flow has four layers:
- Skill registration layer — The manifest declares capabilities, required scopes, and configuration parameters. When the skill loads, OpenClaw registers its tools in the agent’s tool catalog.
- Authentication layer — Manages the OAuth2 authorization code flow, stores tokens, and handles refresh cycles. Every outbound API call passes through this layer.
- API client layer — Wraps Strava’s REST API v3. Maps internal tool calls to HTTP requests, handles pagination, and enforces rate limits.
- Data processing layer — Transforms raw API responses into structured context that Claude can interpret. This includes unit conversion, time-series aggregation, zone classification, and derived metric computation.
The separation matters because failures at each layer produce different symptoms. A token expiration surfaces as a 401 from layer 2. A rate limit hit surfaces as a 429 from layer 3. A malformed activity response surfaces as a parsing error in layer 4. Understanding which layer owns which responsibility is the first step toward effective debugging.
The Strava API Data Model
The Strava API v3 organizes athletic data into several core resources. The OpenClaw Strava skill interacts with most of them.
Activities
An activity is the primary unit of data. Every recorded workout — run, ride, swim, hike — is an activity object. The API returns it as a JSON payload with dozens of fields.
A condensed example of the response from GET /api/v3/activities/{id}:
{
"id": 12345678901,
"name": "Tuesday Recovery Run",
"type": "Run",
"sport_type": "Run",
"start_date": "2026-02-10T07:15:00Z",
"start_date_local": "2026-02-10T07:15:00-08:00",
"timezone": "(GMT-08:00) America/Los_Angeles",
"distance": 8046.72,
"moving_time": 2580,
"elapsed_time": 2700,
"total_elevation_gain": 42.3,
"average_speed": 3.12,
"max_speed": 4.18,
"average_heartrate": 138.4,
"max_heartrate": 162,
"has_heartrate": true,
"average_cadence": 172.5,
"suffer_score": 64,
"calories": 485,
"map": {
"id": "a12345678901",
"summary_polyline": "encodedPolylineString...",
"resource_state": 2
},
"splits_metric": [
{
"distance": 1000.0,
"elapsed_time": 318,
"elevation_difference": 3.2,
"moving_time": 315,
"average_heartrate": 134.2,
"pace_zone": 2
}
],
"segment_efforts": [],
"device_watts": false,
"average_watts": null,
"weighted_average_watts": null
}
Key fields the skill extracts:
- distance — Reported in meters. The skill converts to kilometers or miles based on the user’s locale preference.
- moving_time vs. elapsed_time — Moving time excludes pauses (traffic lights, water stops). The delta between the two indicates total stopped time.
- average_speed / max_speed — In meters per second. Converted to pace (min/km or min/mile) for running activities, or km/h and mph for cycling.
- average_heartrate / max_heartrate — Beats per minute. Present only if the recording device transmitted HR data (
has_heartrate: true). - suffer_score — Strava’s proprietary training load metric, available on premium accounts. An integer representing relative effort.
- splits_metric — Per-kilometer breakdown with pace, elevation delta, and heart rate. Essential for split-by-split analysis.
The skill makes a distinction between the summary representation (returned by list endpoints like GET /api/v3/athlete/activities) and the detailed representation (returned by the single-activity endpoint). The summary omits splits, segment efforts, and the full map polyline. When a user asks for detailed information about a specific workout, the skill must issue a second request for the detailed payload.
Segments
A Strava segment is a user-defined section of road or trail. Segments have leaderboards, personal records, and historical effort data.
The segment_efforts array inside a detailed activity lists every segment the athlete traversed during the workout:
{
"segment_efforts": [
{
"id": 987654321,
"name": "Hawk Hill Climb",
"elapsed_time": 482,
"moving_time": 478,
"start_date": "2026-02-10T07:42:12Z",
"distance": 2120.5,
"average_heartrate": 171.3,
"max_heartrate": 184,
"pr_rank": 2,
"segment": {
"id": 229781,
"name": "Hawk Hill Climb",
"distance": 2120.5,
"average_grade": 6.1,
"maximum_grade": 14.2,
"elevation_high": 220.4,
"elevation_low": 91.8,
"climb_category": 3
}
}
]
}
The pr_rank field is particularly useful. A value of 1 means the effort was a personal record. 2 means second-best. 3 means third-best. null means the effort did not rank in the athlete’s top three. The skill uses this field to answer “Did I PR?” questions without scanning the full effort history.
Streams
Streams are the highest-resolution data Strava offers. A stream is a time-series array of values sampled at regular intervals throughout an activity. Available stream types include:
| Stream type | Description | Unit |
|---|---|---|
time | Seconds since activity start | seconds |
distance | Cumulative distance | meters |
latlng | GPS coordinates | [lat, lng] |
altitude | Elevation | meters |
heartrate | Heart rate | bpm |
cadence | Steps or revolutions per minute | rpm/spm |
watts | Power output (cycling) | watts |
temp | Temperature | celsius |
velocity_smooth | Smoothed speed | m/s |
grade_smooth | Smoothed road gradient | percent |
The endpoint GET /api/v3/activities/{id}/streams accepts a keys parameter specifying which stream types to retrieve:
{
"keys": ["time", "heartrate", "watts", "latlng", "altitude"],
"resolution": "high"
}
The response is an array of stream objects, each containing a data array with one value per sample point:
[
{
"type": "time",
"data": [0, 1, 2, 3, 4, 5],
"series_type": "time",
"original_size": 9842,
"resolution": "high"
},
{
"type": "heartrate",
"data": [112, 114, 118, 122, 125, 128],
"series_type": "distance",
"original_size": 9842,
"resolution": "high"
}
]
Stream data is where advanced analysis becomes possible. The skill uses streams for heart rate zone distribution, power curve computation, GPS route matching, and elevation profile analysis.
Zones
Strava organizes heart rate into five zones based on the athlete’s configured max heart rate:
{
"heart_rate": {
"custom_zones": false,
"zones": [
{ "min": 0, "max": 115 },
{ "min": 115, "max": 138 },
{ "min": 138, "max": 155 },
{ "min": 155, "max": 172 },
{ "min": 172, "max": -1 }
]
}
}
The zone boundaries come from GET /api/v3/athlete/zones. The -1 max on zone 5 indicates no upper bound. These boundaries are athlete-specific — they depend on the max heart rate and resting heart rate configured in Strava settings.
The skill retrieves zone configuration once during the OAuth handshake and caches it locally. When computing zone distribution from a heart rate stream, it applies these boundaries to each sample point and calculates the percentage of time spent in each zone.
Token Management and the OAuth2 Lifecycle
The Strava API uses OAuth2 with the authorization code grant. The token lifecycle has three phases: initial authorization, access token usage, and refresh.
Initial Authorization
When a user first connects their Strava account, the skill generates an authorization URL:
https://www.strava.com/oauth/authorize
?client_id=SKILL_CLIENT_ID
&redirect_uri=CALLBACK_URL
&response_type=code
&scope=read,activity:read_all,profile:read_all
&approval_prompt=auto
The scope parameter determines what data the skill can access. The activity:read_all scope is critical — without it, the skill can only see activities marked as public. Most training data is stored in private activities. The profile:read_all scope grants access to heart rate zones and athlete metadata.
After the user approves, Strava redirects to the callback URL with an authorization code. The skill exchanges that code for an access token and a refresh token:
{
"token_type": "Bearer",
"access_token": "a1b2c3d4e5f6...",
"refresh_token": "f6e5d4c3b2a1...",
"expires_at": 1739318400,
"expires_in": 21600
}
The expires_at field is a Unix timestamp. Strava access tokens expire after six hours (21,600 seconds). The skill stores both tokens in the agent’s encrypted credential store.
Token Refresh
Before every API call, the authentication layer checks whether the stored access token has expired. If Date.now() / 1000 >= expires_at, the skill issues a refresh request:
POST https://www.strava.com/oauth/token
client_id=SKILL_CLIENT_ID
client_secret=SKILL_CLIENT_SECRET
grant_type=refresh_token
refresh_token=f6e5d4c3b2a1...
The response contains a new access token, a new refresh token, and an updated expiration timestamp. Both tokens are overwritten in storage. This cycle repeats indefinitely as long as the user does not revoke the application in their Strava settings.
A subtle detail: the refresh token itself rotates on every refresh. The old refresh token becomes invalid the moment a new one is issued. If the skill stores the new access token but fails to persist the new refresh token (due to a crash or storage error), the entire authorization chain breaks. The user must re-authorize from scratch. The skill handles this by writing both tokens atomically in a single storage operation.
Token Validation Strategy
Rather than checking expiration before every single request, the skill uses a two-tier approach:
- Preemptive refresh — If the access token will expire within the next 300 seconds, the skill proactively refreshes it before making the API call. This prevents failures during multi-step operations where a token might expire between the first and third request.
- Reactive refresh — If a 401 response arrives despite the token appearing valid (possible due to clock skew between the agent’s system and Strava’s servers), the skill triggers an immediate refresh and retries the failed request once.
This combination minimizes both unnecessary refresh calls and unexpected authorization failures.
Rate Limiting Strategies
Strava enforces two rate limits:
- 15-minute bucket: 100 requests per 15 minutes
- Daily bucket: 1,000 requests per day
These limits apply per application (client ID), not per user. If multiple users share the same skill deployment with the same client credentials, their requests count toward the same bucket.
Tracking Usage
The API returns rate limit headers with every response:
X-RateLimit-Limit: 100,1000
X-RateLimit-Usage: 34,287
The first value in each comma-separated pair is the 15-minute limit/usage. The second is the daily limit/usage. The skill parses these headers after every API call and stores the current usage counts in memory.
Backoff Strategy
When usage approaches a threshold (80% of either bucket), the skill switches to a conservation mode:
- Batch requests — Instead of fetching activities one at a time, the skill uses the list endpoint with appropriate pagination to retrieve multiple activities in a single call.
- Cache aggressively — Activity data that has already been fetched is cached for the duration of the session. Repeated questions about the same workout do not trigger additional API calls.
- Defer non-critical requests — If a user asks for both a recent activity summary and a segment leaderboard, and the rate budget is constrained, the skill prioritizes the activity summary and defers the leaderboard fetch.
If the 15-minute bucket is exhausted (a 429 response), the skill calculates the remaining wait time from the header data, informs the user, and queues the request for automatic retry. The daily limit is harder to recover from — if exhausted, the skill switches to cached-only mode for the rest of the day and surfaces a clear message explaining the limitation.
Minimizing API Calls
Several design decisions reduce the total number of API calls per user interaction:
- Summary-first fetching — The skill retrieves the activity list (summary representations) first. Only if the user’s query requires detailed data (splits, segments, streams) does it fetch the full activity.
- Selective stream retrieval — The
keysparameter on the streams endpoint allows the skill to request only the stream types relevant to the query. A question about heart rate zones fetches only theheartrateandtimestreams, not the full set of GPS, altitude, cadence, and power data. - Zone caching — Athlete zone configuration changes infrequently. The skill caches it for 24 hours, avoiding a round trip on every zone calculation.
Data Processing Pipeline
Raw Strava API data arrives in SI units with UTC timestamps. The processing layer transforms this into contextually useful information.
Unit Conversion
The skill maintains a conversion table keyed by activity type and user locale:
| Metric | Running (imperial) | Running (metric) | Cycling (imperial) | Cycling (metric) |
|---|---|---|---|---|
| Distance | miles | kilometers | miles | kilometers |
| Pace/Speed | min/mile | min/km | mph | km/h |
| Elevation | feet | meters | feet | meters |
Speed-to-pace conversion for running activities is non-trivial because the relationship is inverse. A speed of 3.0 m/s translates to a pace of 5:33/km, computed as 1000 / speed / 60 for minutes and (1000 / speed) % 60 for seconds.
Heart Rate Zone Distribution
Given a heart rate stream and the athlete’s zone boundaries, the skill computes time-in-zone by iterating through each sample point:
{
"zone_1": { "seconds": 180, "percent": 6.2 },
"zone_2": { "seconds": 1620, "percent": 55.9 },
"zone_3": { "seconds": 780, "percent": 26.9 },
"zone_4": { "seconds": 285, "percent": 9.8 },
"zone_5": { "seconds": 35, "percent": 1.2 }
}
This computation requires both the heartrate and time streams. Each heart rate sample is classified into its zone, and the corresponding time delta (difference between consecutive time stream values) is accumulated for that zone. The output is what Claude uses to answer questions like “Did I stay in zone 2?” or “How much time did I spend in threshold?”
Split Analysis
For running activities, the splits_metric array provides per-kilometer data. The skill augments this with derived metrics:
- Pace deviation — The difference between each split’s pace and the overall average. Highlights positive and negative splits.
- Cardiac drift — The change in heart rate between the first and last third of the activity at equivalent pace. An indicator of aerobic decoupling and dehydration.
- Grade-adjusted pace (GAP) — Adjusts each split’s pace based on the elevation change. A 6:00/km split on a 5% grade is considerably harder than the same pace on flat terrain. The skill applies a standard GAP formula that adds roughly 5 seconds per percent grade for uphills and subtracts 2-3 seconds for downhills.
Activity Classification
Not every question maps directly to a single API field. When a user asks “How was my run?”, the skill assembles a composite assessment:
- Retrieve the latest running activity (filter by
type=Runfrom the activity list). - Fetch the detailed representation for splits and segment efforts.
- If heart rate data is present, compute zone distribution.
- Compare key metrics (pace, distance, heart rate) against the athlete’s recent history (last 4 weeks of activities of the same type).
- Package the results into a structured summary that Claude can narrate.
The comparison against recent history is what turns raw data into insight. A 5:15/km average pace is meaningless in isolation. Knowing that it is 12 seconds faster than the athlete’s 4-week average for easy runs, and that heart rate was 8 bpm higher than usual, suggests the effort was harder than intended.
Advanced Features
GPS Stream Analysis
The latlng stream provides latitude/longitude pairs at each sample point. The skill uses this data for several purposes:
Route matching. Given two activities, the skill compares their GPS traces to determine if they followed the same route. This is done by computing the Hausdorff distance between the two point sets — if the maximum deviation is under a configurable threshold (default: 50 meters), the routes are considered equivalent. Route matching enables comparisons like “How does today’s run compare to the same route last week?”
Segment identification. Even without explicit segment effort data, GPS coordinates can identify which segments were traversed. The skill maintains a local index of frequently queried segments (by their start/end coordinates) and matches incoming GPS traces against it.
Elevation profile reconstruction. The altitude stream, paired with distance, produces an elevation profile. The skill computes total ascent, total descent, and identifies significant climbs (defined as sustained elevation gain exceeding 30 meters over fewer than 2 kilometers).
Power Data Processing (Cycling)
For cycling activities recorded with a power meter, the watts stream is available. The skill computes several standard cycling metrics:
- Normalized Power (NP) — A rolling 30-second average of power, raised to the fourth power, averaged, then rooted. Accounts for the physiological cost of variable effort. The formula:
NP = (mean(rolling_30s_avg^4))^0.25. - Intensity Factor (IF) — Normalized Power divided by the athlete’s Functional Threshold Power (FTP). Requires the athlete to have their FTP configured.
- Training Stress Score (TSS) —
TSS = (duration_seconds * NP * IF) / (FTP * 3600) * 100. Represents the total training load of the ride. - Variability Index (VI) —
NP / average_power. Indicates how variable the effort was. A perfectly steady effort has a VI of 1.0; a highly variable effort (intervals, surges, stops) produces a VI above 1.1.
These metrics are computed from the raw watts stream. They are not available directly from the Strava API — Strava provides average_watts and weighted_average_watts (which is essentially NP), but TSS and IF require athlete-specific FTP data that the skill stores locally.
Training Load Estimation
For athletes without power meters (most runners), the skill estimates training load using heart rate data and the TRIMP (Training Impulse) method:
TRIMP = duration_minutes * avg_HR_fraction * 0.64 * e^(1.92 * avg_HR_fraction)
Where avg_HR_fraction = (avg_HR - resting_HR) / (max_HR - resting_HR).
The skill retrieves resting heart rate and max heart rate from the athlete’s Strava profile (or uses defaults of 60 bpm and 190 bpm if not configured). TRIMP values are computed for each activity and accumulated over rolling 7-day and 28-day windows to track acute and chronic training load.
The ratio of acute to chronic load (the Acute-to-Chronic Workload Ratio, or ACWR) serves as a basic injury risk indicator:
- ACWR below 0.8 — Undertraining. Fitness may be declining.
- ACWR between 0.8 and 1.3 — Optimal range. Training load is appropriate.
- ACWR above 1.5 — Elevated injury risk. Training load increased too rapidly.
When a user asks about training load or readiness, the skill computes ACWR from cached activity data and includes it in the context provided to Claude.
Performance Considerations
Response Latency
A typical interaction involves 1-3 API calls. The activity list endpoint responds in 150-300ms. The detailed activity endpoint responds in 200-400ms. Stream endpoints respond in 300-600ms depending on activity duration (longer activities produce larger stream payloads).
Total latency for a complex query (activity detail + streams + zone calculation) is typically under 1.5 seconds. This sits well within acceptable response times for a chat-based interface. The primary bottleneck is rarely the Strava API itself — it is the Claude inference time that follows.
Payload Size
Stream data for a 2-hour cycling activity can contain 7,000+ sample points per stream type. Requesting five stream types produces a combined payload exceeding 500KB. The skill does not pass raw stream data to Claude. Instead, it processes the streams into summary statistics (zone distribution, NP, TSS, split analysis) and provides those summaries as structured context.
This design choice keeps the token count in the LLM context manageable and avoids wasting Claude’s context window on thousands of raw numerical values that it would need to re-aggregate anyway.
Caching Strategy
The skill implements a three-tier cache:
- Session cache — Activities fetched during the current conversation are held in memory. A follow-up question about the same run does not trigger another API call.
- Local cache — The last 50 activities (summary representations) are persisted to disk with a 1-hour TTL. When a user asks “show me my recent runs,” the skill serves from cache if the data is fresh.
- Derived metric cache — Computed values like TRIMP, NP, and zone distributions are cached alongside the activity they derive from. Recomputation occurs only when the underlying activity data changes (which, for completed activities, is never).
Cache invalidation is straightforward because completed activities are immutable. Once an activity is uploaded and processed by Strava, its data does not change. The only exception is if the user edits the activity (changes the name, crops the GPS track, or marks it as private), which is rare enough that the 1-hour TTL handles it adequately.
Error Handling
The skill defines error categories and handling strategies for each:
| Error | HTTP Code | Cause | Resolution |
|---|---|---|---|
| Token expired | 401 | Access token past expires_at | Automatic refresh and retry |
| Token revoked | 401 | User deauthorized the app | Prompt re-authorization |
| Rate limited (15-min) | 429 | Exceeded 100 requests in 15 minutes | Wait for bucket reset, inform user |
| Rate limited (daily) | 429 | Exceeded 1,000 requests in 24 hours | Switch to cached-only mode |
| Activity not found | 404 | Invalid activity ID or deleted activity | Surface clear message to user |
| Scope insufficient | 403 | Missing required OAuth scope | Prompt re-authorization with correct scopes |
| Server error | 500/503 | Strava API outage | Retry with exponential backoff (max 3 attempts) |
For 500-series errors, the skill retries with exponential backoff: 1 second, then 2 seconds, then 4 seconds. After three failures, it reports that the Strava API is temporarily unreachable and suggests the user try again in a few minutes.
How Claude Interprets the Data
The data processing layer produces structured JSON that gets injected into Claude’s context as tool results. Claude does not see raw API responses. It sees pre-processed summaries shaped for natural language generation.
An example of the structured context provided to Claude after a “How was my run?” query:
{
"activity": {
"name": "Tuesday Recovery Run",
"type": "Run",
"date": "2026-02-10",
"distance_display": "5.0 miles",
"moving_time_display": "43:00",
"pace_display": "8:36/mile",
"elevation_gain_display": "139 ft"
},
"heart_rate": {
"average": 138,
"max": 162,
"zone_distribution": {
"zone_1_percent": 6.2,
"zone_2_percent": 55.9,
"zone_3_percent": 26.9,
"zone_4_percent": 9.8,
"zone_5_percent": 1.2
}
},
"comparison": {
"vs_4_week_avg": {
"pace_delta": "-0:12/mile faster",
"hr_delta": "+8 bpm higher",
"distance_delta": "+0.3 miles longer"
},
"vs_same_route_last": {
"pace_delta": "-0:05/mile faster",
"hr_delta": "+3 bpm higher"
}
},
"flags": [
"Heart rate 26.9% in zone 3 -- higher than typical for a recovery run",
"Pace faster than 4-week easy run average"
]
}
The flags array is significant. The data processing layer encodes domain-specific heuristics that Claude can reference. A flag like “Heart rate 26.9% in zone 3 — higher than typical for a recovery run” gives Claude explicit context to generate actionable advice without relying entirely on the model’s general fitness knowledge.
This design keeps the LLM’s role focused on communication — translating structured athletic data into conversational responses — rather than on numerical computation, which is better handled deterministically in code.
Extending the Skill
The OpenClaw Strava skill is open source. Developers who want to add functionality can extend it in several ways:
- Custom derived metrics — Add new computations in the data processing layer. Efficiency Factor (pace divided by heart rate), decoupling analysis, and running power estimation are common extensions.
- Additional API endpoints — Strava offers endpoints for routes, clubs, and athlete stats that the current skill does not fully utilize. Wrapping these endpoints follows the same pattern as existing handlers.
- Cross-skill integration — The skill exposes processed activity data that other skills can consume. A habit-tracker skill can listen for new activities and auto-log them. A cal-com skill can compare planned workouts against actual performance.
The skill’s modular architecture makes these extensions straightforward. Each new capability is a tool definition, a handler function, and optionally a new processing function in the data layer.
Frequently Asked Questions
What Strava API version does the skill target?
The skill targets Strava API v3, which has been stable since 2018. Strava has not announced a v4. All endpoints used by the skill are documented in Strava’s official API reference.
Does the skill require a Strava premium account?
No. The core functionality — activities, segments, streams, zones — works with a free Strava account. Premium-only fields like suffer_score and the full leaderboard data are used when available but are not required.
How does the skill handle activities recorded without heart rate?
Gracefully. If has_heartrate is false in the activity payload, the skill skips zone distribution computation and TRIMP calculation. Claude is informed that heart rate data is unavailable and adjusts its response accordingly, focusing on pace, distance, and elevation instead.
Can the skill write data back to Strava?
The current implementation is read-only. The OAuth scope activity:read_all permits reading all activities. Writing (creating activities, updating names, posting comments) would require the activity:write scope, which is not requested by default. Developers can modify the scope configuration to enable write operations.
What happens if the Strava API changes?
The API client layer abstracts endpoint URLs and response shapes. If Strava modifies a field name or restructures a response, the fix is localized to the client layer. The data processing layer and Claude’s context format remain unchanged.
How much storage does the local cache consume?
Minimal. Fifty cached activity summaries occupy approximately 200KB. Derived metric caches add another 50-100KB. Stream data is not cached to disk — only the computed summaries are persisted.
Can I use this with multiple Strava accounts?
Each OpenClaw agent instance connects to one Strava account. If you need to monitor multiple accounts (coaching scenario), you would deploy separate agent instances, each authorized with a different Strava account.
Is GPS data sent to Claude?
No. Raw GPS coordinates stay in the data processing layer. Claude receives route match results (“same route as last Tuesday”), elevation summaries, and segment identifications — not the underlying latitude/longitude arrays. This keeps context windows lean and avoids sending location data unnecessarily.
Where to Go from Here
For developers interested in contributing to the OpenClaw Strava skill or building similar integrations, the skill source code is available on the strava-connect skill page.
For users who want to install the skill without diving into the internals, the getting started guide covers skill installation basics. The best productivity skills roundup highlights complementary tools that pair well with fitness data — calendar sync, habit tracking, and daily briefings.
Browse the Fitness category for other health and training skills in the ecosystem. The Productivity category lists tools for task management and workflow automation that integrate well with training routines.
Oh My OpenClaw curates 433 skills across 10 categories. The Strava skill is one of the most technically sophisticated in the catalog. Understanding its internals — from OAuth token rotation to GPS stream processing to training load estimation — demonstrates the depth of integration that the OpenClaw framework makes possible.