fitops activities
Import, post, synchronize, browse, and query activities from Strava or local GPX/TCX files.
Output is a formatted table by default. Add --json to any command for raw JSON output (useful for scripting or AI agents).
For running race activities, FitOps can store an official chip time and race distance when the watch GPS distance is wrong. Those overrides are local to FitOps; the original Strava values remain unchanged.
Commands
fitops activities import-mobile <PATH>
Import a FitOps Mobile JSON export into the active desktop athlete's local dataset. The file must use the fitops.activities version 1 format, contain 1–100 activities, and be no larger than 10 MiB. This command never posts to Strava or contacts Huawei.
fitops athlete init --name "Jane Runner"
fitops activities import-mobile ~/Downloads/fitops-export.json --json| Flag | Default | Description |
|---|---|---|
--json | false | Return _meta, imported activity records, source identities, counts, and data availability |
The imported records preserve the original provider, provider account, and activity ID alongside available timestamped heart-rate, distance, and speed samples. Re-importing the same source identity updates that desktop activity; records belonging to another provider, account, or local athlete remain separate. FitOps does not automatically merge a mobile activity with a similar Strava activity. Importing a snapshot does not synchronize deletions.
Desktop calibration remains unchanged. Moving time is unavailable because version 1 exports carry elapsed duration only, so stress and other metrics that require moving time are not copied or invented. VO₂max is computed only when the samples and existing desktop calibration support it. Results can differ from the phone because each application uses its own calibration and calculation methods. The JSON response's data_availability block describes these limits.
When an export has distance samples but no speed samples, FitOps derives speed from consecutive distance/time pairs during import. It does not bridge intervals longer than 30 seconds or decreasing/missing distance readings. Recorded speed takes precedence, and the original speed samples remain available separately from the normalized stream. Desktop VO₂max uses its existing Daniels/Cooper model, while the mobile MVP uses a Daniels-only estimate.
Keep exports private: they contain fitness records and may include heart-rate samples. Neither Huawei tokens nor mobile calibration settings are included in this exchange format.
fitops activities import <PATH>
Import an original GPX or TCX recording, normalize its summary, streams, and laps, and compute the same cached analytics used for synced activities. Every import receives a local analytics stamp after weather and derived metrics are stored, including imports made with --local-only. Posting to Strava is enabled by default: FitOps uploads the selected source file and its generated stamp through the configured headless browser session. The returned Strava activity ID is linked to the local activity before the command finishes.
FitOps keeps the original filename, format, size, and SHA-256 hash for provenance and deduplication, but it does not copy the GPX/TCX payload into the FitOps data directory. Exact re-imports, equivalent GPX/TCX exports, and matching activities already synced from Strava reuse the existing activity instead of creating a duplicate.
Without --name, running activities use Outdoor run and cycling activities use Outdoor cycle. After persistence, FitOps automatically fetches Open-Meteo weather from the recording's start coordinates and time, then stores WBGT, weather-adjusted pace, terrain-and-weather-adjusted true pace, course bearing, and applicable heart-rate heat impact. The import still succeeds when weather is unavailable and reports that status in its output.
fitops athlete init --name "Jane Runner"
fitops activities import ~/Downloads/morning-run.gpx
fitops activities import race.tcx --sport Run --name "City 10K" --json
fitops activities import morning-run.tcx --gear "Daily Trainer"
fitops activities import offline-run.gpx --local-only| Flag | Default | Description |
|---|---|---|
--sport TYPE | auto | Override the inferred sport (Run, Ride, Walk, and so on) |
--name TEXT | Outdoor run or Outdoor cycle | Override the activity title |
--description TEXT | — | Add personal description text above the generated FitOps stamp |
--gear TEXT | — | Attach gear by its stable ID or exact name |
--post-to-strava / --local-only | post | Post after import, or keep the processed activity local only |
--json | false | Return the activity, provenance hash, and inference details as JSON |
Automatic detection prefers sport metadata embedded in TCX, then file/title keywords, then median moving speed. Use --sport whenever the heuristic is wrong. Gear is resolved from the active athlete profile and stored on the local activity. During browser publication, FitOps also selects the matching gear in Strava's upload editor.
The JSON activity object includes a provider-neutral activity_id, a nullable strava_activity_id, and origin (gpx, tcx, or strava). The weather.status field is fetched, already_available, unavailable, or a specific skipped reason such as skipped_no_gps; weather.data contains the stored raw and derived values when available. The publication object reports completed, failed, already_linked, or not_requested. A failed browser post exits non-zero but leaves the processed activity in the local database and includes the reason in publication.error.
fitops activities sync-strava <ACTIVITY_ID>
Connect a local-only imported activity to an activity that is already on Strava. FitOps generates the current stamp, appends it through the configured headless browser session, and stores the supplied Strava ID on the local activity. No GPX/TCX file is required or uploaded.
fitops browser configure --type brave \
--user-data-dir "$HOME/Library/Application Support/BraveSoftware/Brave-Browser" \
--profile Default
fitops activities sync-strava 42 --strava-id 1234567890
fitops activities sync-strava 42 --strava-id 1234567890 --jsonThe command reports Synced with Strava activity …; JSON output uses "action": "sync". The older hidden activities publish --strava-id … spelling remains as a compatibility alias. Browser authentication uses the configured profile and never copies cookies.
fitops activities list
List recent activities with full metrics.
fitops activities list [OPTIONS]Options:
| Flag | Default | Description |
|---|---|---|
--sport TYPE | all | Filter by sport type (e.g. Run, Ride, Swim) |
--limit N | 20 | Max number of activities to return |
--offset N | 0 | Skip the first N results (use with --limit to page through results) |
--after DATE | — | Filter activities after this date (YYYY-MM-DD) |
--before DATE | — | Filter activities before this date (YYYY-MM-DD) |
--search TEXT | — | Case-insensitive substring match on activity name/title |
--tag TAG | — | Filter by tag: race, trainer, commute, manual, private |
--json | false | Output raw JSON instead of the formatted table |
Examples:
fitops activities list
fitops activities list --sport Run --limit 10
fitops activities list --after 2026-01-01
fitops activities list --after 2025-12-01 --before 2026-01-01 # date range
fitops activities list --sport Ride --limit 5 --after 2025-12-01
fitops activities list --search "Morning Run" # title search
fitops activities list --tag race # races only
fitops activities list --tag trainer # indoor trainer rides
fitops activities list --sport Run --json # JSON for scripting or agents
fitops activities list --sport Run --limit 20 --offset 20 # page 2Pagination (JSON mode):
When using --json, the _meta envelope includes pagination fields so agents can detect when more results exist:
{
"_meta": {
"total_count": 150,
"returned_count": 20,
"offset": 0,
"has_more": true
}
}To page through all results, increment --offset by --limit until has_more is false.
Supported tag values:
| Tag | Matches |
|---|---|
race | Activities with Strava workout_type = 1 (race) |
trainer | Indoor trainer sessions |
commute | Commute activities |
manual | Manually entered activities |
private | Private activities |
See Output Examples → Activities for sample output.

fitops activities get <ID>
Get detailed info for a single activity.
The formatted detail view shows True Pace as the effort-normalised pace metric when weather and streams are available.
fitops activities get 12345678901 [OPTIONS]Arguments:
| Argument | Description |
|---|---|
ID | Local FitOps activity ID or legacy Strava activity ID (required) |
Options:
| Flag | Default | Description |
|---|---|---|
--fresh | false | Re-fetch detail and streams from Strava, replace local stream cache, and recompute derived activity metrics from the fresh payload |
--splits | false | Print a per-km splits table (runs only, requires streams) |
--workout | false | Print focused workout plan + compliance view |
--chart | false | Render an ASCII time-series chart in the terminal |
--stream STREAM | heartrate | Stream to plot when --chart is set (see table below) |
--x-axis VALUE | time | Chart x-axis: time or distance |
--from N | — | Zoom window start (seconds or metres) |
--to N | — | Zoom window end (seconds or metres) |
--width N | auto | Chart width in characters |
--height N | 20 | Chart height in rows |
--resolution N | auto | Number of data buckets (lower = smoother) |
--json | false | Output raw JSON instead of formatted output |
Flags can be combined: --workout --splits shows per-km splits segmented by workout intervals.
Chart streams:
| Stream | Display | Notes |
|---|---|---|
heartrate | Heart Rate (bpm) | |
pace / velocity_smooth | Pace (min/km) | Y-axis inverted: faster = higher |
speed | Speed (km/h) | Auto-selected for cycling when velocity_smooth is requested |
gap | Grade Adj. Pace (min/km) | Derived — requires velocity_smooth + grade_smooth |
wap | Weighted Avg Pace (min/km) | Derived — 30-sample rolling mean of pace |
true_pace | True Pace (min/km) | Derived — GAP + weather-adjusted pace (most accurate effort metric) |
altitude | Altitude (m) | |
cadence | Cadence (spm) | |
watts | Power (W) | |
distance | Distance (m) |
Examples:
# Full detail view
fitops activities get 17985851162
# Per-km splits table
fitops activities get 17985851162 --splits
# Workout plan compliance + segment detail
fitops activities get 17985851162 --workout
# Workout splits segmented by intervals
fitops activities get 17985851162 --workout --splits
# ASCII heart rate chart
fitops activities get 17985851162 --chart
# True pace chart over distance
fitops activities get 17985851162 --chart --stream true_pace --x-axis distance
# Zoom into km 5–10
fitops activities get 17985851162 --chart --stream heartrate --x-axis distance --from 5000 --to 10000
# JSON for scripting / agents
fitops activities get 17985851162 --jsonJSON output shape (for agents and scripting):
When --json is used, the output is a single activity object enriched with all analytics available in the dashboard. Top-level keys:
| Key | Type | Description |
|---|---|---|
activity_id | int | Stable local FitOps activity ID |
strava_activity_id | int | Strava activity ID |
origin | string | strava, gpx, or tcx |
name | string | Activity title |
sport_type | string | e.g. "Run", "Ride" |
start_date_local | string | Local datetime of start |
timezone | string | IANA timezone (e.g. "Europe/London") |
duration | object | moving_time_seconds, moving_time_formatted, elapsed_time_seconds, elapsed_time_formatted, efficiency_pct |
distance | object | km |
pace | object|null | average_per_km, average_per_mile (runs only) |
speed | object | average_kmh, max_kmh |
elevation | object | total_gain_m |
heart_rate | object|null | average_bpm, max_bpm |
cadence | object|null | average_spm |
power | object|null | Rides: average_watts, max_watts, weighted_average_watts. Runs: avg_w, max_w, np_w, est_kcal, source (estimated from pace stream — see Estimated Running Power) |
training_metrics | object | suffer_score, calories, training_stress_score |
equipment | object | gear_id, gear_name, gear_type |
description | string|null | Activity description (whitespace-trimmed) |
device_name | string|null | Recording device (e.g. "Garmin Forerunner 965") |
flags | object | trainer, commute, manual, private, is_race (only keys that are true are included) |
social | object | kudos, comments (omitted when both are 0) |
data_availability | object | has_gps, has_heart_rate, has_power, streams_fetched, laps_fetched, detail_fetched |
insights | object | See Insights block below |
analytics | object|null | See Analytics block below (requires streams) |
km_splits | array|null | Per-km splits (runs only, requires streams) — see km_splits below |
avg_gap | string|null | Average grade-adjusted pace, e.g. "4:52/km" (runs only, requires streams) |
laps | array|null | Lap-by-lap breakdown (requires laps fetched) — see laps below |
weather | object|null | Weather conditions + pace adjustments (requires weather fetch) — see weather below |
performance_insights | array|null | Detected new records or metric changes (requires streams) — see performance_insights below |
workout | object|null | Linked workout plan + compliance (if workout was linked) — see workout below |
race_result | object|null | Official race result override state for running race activities — see race_result below |
Insights block
"insights": {
"aerobic_training_score": 3.8,
"aerobic_label": "Strong aerobic stimulus",
"anaerobic_training_score": 2.1,
"anaerobic_label": "Moderate anaerobic load",
"hr_drift": {
"decoupling_pct": 4.2,
"first_half_ratio": 0.98,
"second_half_ratio": 1.02
}
}hr_drift is only present when HR + velocity streams are available.
Aerobic/anaerobic score labels:
| Score range | Aerobic label | Anaerobic label |
|---|---|---|
| ≥ 4.5 | Exceptional aerobic session | Race-intensity effort |
| ≥ 3.5 | Strong aerobic stimulus | Hard anaerobic session |
| ≥ 2.5 | Solid aerobic base work | Significant threshold stress |
| ≥ 1.5 | Moderate aerobic benefit | Moderate anaerobic load |
| ≥ 0.5 | Light aerobic stimulus | Light anaerobic stimulus |
| < 0.5 | Minimal aerobic benefit | Minimal anaerobic stress |
Analytics block
Requires streams to be fetched. Contains HR and pace time-in-zone breakdowns:
"analytics": {
"hr_zones": [
{ "zone": 1, "name": "Recovery", "min_bpm": 100, "max_bpm": 130, "time_s": 600, "time_fmt": "10:00", "pct": 12 }
],
"pace_zones": [
{ "zone": 1, "name": "Easy", "min_pace": "6:00", "max_pace": "7:30", "time_s": 1800, "time_fmt": "30:00", "pct": 50 }
],
"lt2_bpm": 165,
"vo2max": 52.4
}km_splits
Per-km breakdown (runs only, requires streams):
"km_splits": [
{ "km": 1, "label": "1", "partial": false, "pace": "4:52", "pace_s": 292.0, "avg_true_pace": "4:58/km", "avg_hr": 155, "avg_cad": 174, "elev_gain": 8, "elev_loss": 2 },
{ "km": 5, "label": "5 (0.32km)", "partial": true, "pace": "4:45", "pace_s": 285.0, "avg_true_pace": null, "avg_hr": 162, "avg_cad": 176, "elev_gain": 3, "elev_loss": 0 }
]The last split is marked "partial": true when the final km is incomplete. avg_true_pace is null when no true_pace stream data is available (e.g., no weather data). elev_loss is the total descent in metres for the km segment.
When a race activity has an official chip time or race distance override, km_splits are recomputed from the corrected race result rather than the raw GPS distance.
race_result
"race_result": {
"override_active": true,
"recorded_distance_km": 9.82,
"race_distance_km": 10.0,
"recorded_time_formatted": "41:00",
"chip_time_formatted": "40:30",
"corrected_avg_pace_formatted": "4:03/km",
"distance_correction_factor": 1.01833,
"time_correction_factor": 0.987805
}Use these commands to manage the override:
fitops activities set-race-result 17985851162 --chip-time 40:30 --race-distance-km 10
fitops activities clear-race-result 17985851162laps
"laps": [
{ "index": 1, "name": "Lap 1", "moving_time_s": 360, "distance_km": 1.0, "pace": "6:00/km", "pace_s": 360.0, "avg_hr": 148, "max_hr": 158, "avg_watts": null }
]weather
"weather": {
"temperature_c": 22.0,
"apparent_temp_c": 21.0,
"humidity_pct": 65.0,
"wind_speed_ms": 3.5,
"wind_speed_kmh": 12.6,
"wind_direction_deg": 270.0,
"wind_dir_compass": "W",
"precipitation_mm": 0.0,
"wbgt_c": 18.4,
"wbgt_flag": "yellow",
"pace_heat_factor": 1.03,
"condition": "Partly cloudy",
"temp_fmt": "22°C",
"wap_factor": 1.038,
"wap_factor_pct": 3.8,
"wap_fmt": "5:02/km",
"true_pace_fmt": "5:05/km",
"course_bearing": 214.0,
"hr_heat_pct": 2.1,
"hr_heat_bpm": 3,
"source": "open-meteo"
}WBGT heat flags: "green" (< 18 °C) / "yellow" (18–23 °C) / "red" (23–28 °C) / "black" (> 28 °C).
wap_factor > 1 means conditions were harder than neutral. wap_fmt is the weather-adjusted equivalent pace at neutral conditions.
performance_insights
Detected new records or significant metric changes:
"performance_insights": [
{
"metric": "max_hr",
"label": "New max heart rate detected",
"setting_key": "max_hr",
"current_value": 188,
"detected_value": 192,
"delta_pct": 2.1,
"action": "update",
"detected_fmt": "192 bpm",
"current_fmt": "188 bpm",
"unit": "bpm",
"explanation": "This run reached a higher heart rate than your stored max."
}
]workout
Present only when a workout plan was linked to this activity via fitops workouts link:
"workout": {
"id": "threshold-tuesday",
"name": "Threshold Tuesday",
"compliance_score": 0.87,
"compliance_pct": 87,
"segments": [
{ "name": "Warm-up", "duration_s": 600, "compliance_pct": 95, ... }
]
}fitops activities stamp
Embed FitOps analytics into an activity description as a formatted footer. Imported activities are stamped locally; Strava activities are updated through the API.
fitops activities stamp [OPTIONS]Options:
| Flag | Default | Description |
|---|---|---|
--id STRAVA_ID | — | Stamp a single activity by Strava ID |
--local-id ACTIVITY_ID | — | Stamp an imported activity locally by FitOps ID |
--all | false | Stamp all activities that haven't been stamped yet |
--force | false | Re-stamp even if already stamped (fetches fresh description from Strava) |
--json | false | Output result as JSON |
Requirements: --id and --all require the activity:write OAuth scope. --local-id works offline and does not require Strava authentication.
JSON output shape:
{
"stamped": [12345678, 12345679],
"skipped": [],
"failed": []
}stamped— Strava IDs of activities successfully updatedskipped— IDs skipped because they were already stamped or already contain the FitOps stamp header (without--force)failed— IDs that failed (e.g. Strava API error)
Examples:
# Stamp a single activity
fitops activities stamp --id 12345678
# Stamp an imported activity without contacting Strava
fitops activities stamp --local-id 42
# Backfill all unstamped activities
fitops activities stamp --all
# Re-stamp everything (refresh all footers)
fitops activities stamp --all --force
# Machine-readable output
fitops activities stamp --all --jsonThe stamp footer contains FitOps-only analytics such as aerobic/anaerobic scores, VO2max estimate, power (real or estimated), weather-adjusted pace or speed, race calibration, linked workout segment details, and the activity-day training load snapshot (CTL, ATL, TSB, and form label) when a cached snapshot exists for that date. Aerobic and anaerobic scores appear only when the activity has recorded average heart-rate data; they are omitted instead of showing pace-based fallback estimates when heart rate is unavailable. Cycling activities label the weather-adjusted value as speed while running activities label it as pace. For runs, stamping refreshes estimated power from true_pace when weather and stream data are available, even if an earlier sync computed power from gap_pace. Linked workout segments show true pace whenever that segment has true pace data, even when the displayed true pace matches the raw segment pace. Basic activity details already shown by Strava, such as distance, elapsed time, pace, speed, and heart rate, are not repeated unless they are part of a richer FitOps-derived section.
Training load values are read from analytics_snapshots for the activity date. Stamping does not recompute CTL/ATL/TSB; if no cached snapshot exists for that activity day, the form section is omitted.
If the activity already has a FitOps stamp, --force replaces it while preserving the user-written description above the stamp. Without --force, FitOps skips activities that already contain the FitOps stamp header, even if the local stamped_at marker has not been set yet.
Sport Type Values
Common Strava sport types: Run, TrailRun, Ride, VirtualRide, Swim, Walk, Hike, WeightTraining, Yoga, Workout
See Also
- Output Examples → Activities — sample output
fitops sync run— fetch activities from Strava