Skip to content

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.

bash
fitops athlete init --name "Jane Runner"
fitops activities import-mobile ~/Downloads/fitops-export.json --json
FlagDefaultDescription
--jsonfalseReturn _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.

bash
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
FlagDefaultDescription
--sport TYPEautoOverride the inferred sport (Run, Ride, Walk, and so on)
--name TEXTOutdoor run or Outdoor cycleOverride 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-onlypostPost after import, or keep the processed activity local only
--jsonfalseReturn 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.

bash
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 --json

The 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.

bash
fitops activities list [OPTIONS]

Options:

FlagDefaultDescription
--sport TYPEallFilter by sport type (e.g. Run, Ride, Swim)
--limit N20Max number of activities to return
--offset N0Skip 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
--jsonfalseOutput raw JSON instead of the formatted table

Examples:

bash
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 2

Pagination (JSON mode):

When using --json, the _meta envelope includes pagination fields so agents can detect when more results exist:

json
{
  "_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:

TagMatches
raceActivities with Strava workout_type = 1 (race)
trainerIndoor trainer sessions
commuteCommute activities
manualManually entered activities
privatePrivate activities

See Output Examples → Activities for sample output.

fitops activities list


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.

bash
fitops activities get 12345678901 [OPTIONS]

Arguments:

ArgumentDescription
IDLocal FitOps activity ID or legacy Strava activity ID (required)

Options:

FlagDefaultDescription
--freshfalseRe-fetch detail and streams from Strava, replace local stream cache, and recompute derived activity metrics from the fresh payload
--splitsfalsePrint a per-km splits table (runs only, requires streams)
--workoutfalsePrint focused workout plan + compliance view
--chartfalseRender an ASCII time-series chart in the terminal
--stream STREAMheartrateStream to plot when --chart is set (see table below)
--x-axis VALUEtimeChart x-axis: time or distance
--from N—Zoom window start (seconds or metres)
--to N—Zoom window end (seconds or metres)
--width NautoChart width in characters
--height N20Chart height in rows
--resolution NautoNumber of data buckets (lower = smoother)
--jsonfalseOutput raw JSON instead of formatted output

Flags can be combined: --workout --splits shows per-km splits segmented by workout intervals.

Chart streams:

StreamDisplayNotes
heartrateHeart Rate (bpm)
pace / velocity_smoothPace (min/km)Y-axis inverted: faster = higher
speedSpeed (km/h)Auto-selected for cycling when velocity_smooth is requested
gapGrade Adj. Pace (min/km)Derived — requires velocity_smooth + grade_smooth
wapWeighted Avg Pace (min/km)Derived — 30-sample rolling mean of pace
true_paceTrue Pace (min/km)Derived — GAP + weather-adjusted pace (most accurate effort metric)
altitudeAltitude (m)
cadenceCadence (spm)
wattsPower (W)
distanceDistance (m)

Examples:

bash
# 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 --json

JSON 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:

KeyTypeDescription
activity_idintStable local FitOps activity ID
strava_activity_idintStrava activity ID
originstringstrava, gpx, or tcx
namestringActivity title
sport_typestringe.g. "Run", "Ride"
start_date_localstringLocal datetime of start
timezonestringIANA timezone (e.g. "Europe/London")
durationobjectmoving_time_seconds, moving_time_formatted, elapsed_time_seconds, elapsed_time_formatted, efficiency_pct
distanceobjectkm
paceobject|nullaverage_per_km, average_per_mile (runs only)
speedobjectaverage_kmh, max_kmh
elevationobjecttotal_gain_m
heart_rateobject|nullaverage_bpm, max_bpm
cadenceobject|nullaverage_spm
powerobject|nullRides: 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_metricsobjectsuffer_score, calories, training_stress_score
equipmentobjectgear_id, gear_name, gear_type
descriptionstring|nullActivity description (whitespace-trimmed)
device_namestring|nullRecording device (e.g. "Garmin Forerunner 965")
flagsobjecttrainer, commute, manual, private, is_race (only keys that are true are included)
socialobjectkudos, comments (omitted when both are 0)
data_availabilityobjecthas_gps, has_heart_rate, has_power, streams_fetched, laps_fetched, detail_fetched
insightsobjectSee Insights block below
analyticsobject|nullSee Analytics block below (requires streams)
km_splitsarray|nullPer-km splits (runs only, requires streams) — see km_splits below
avg_gapstring|nullAverage grade-adjusted pace, e.g. "4:52/km" (runs only, requires streams)
lapsarray|nullLap-by-lap breakdown (requires laps fetched) — see laps below
weatherobject|nullWeather conditions + pace adjustments (requires weather fetch) — see weather below
performance_insightsarray|nullDetected new records or metric changes (requires streams) — see performance_insights below
workoutobject|nullLinked workout plan + compliance (if workout was linked) — see workout below
race_resultobject|nullOfficial race result override state for running race activities — see race_result below

Insights block ​

json
"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 rangeAerobic labelAnaerobic label
≥ 4.5Exceptional aerobic sessionRace-intensity effort
≥ 3.5Strong aerobic stimulusHard anaerobic session
≥ 2.5Solid aerobic base workSignificant threshold stress
≥ 1.5Moderate aerobic benefitModerate anaerobic load
≥ 0.5Light aerobic stimulusLight anaerobic stimulus
< 0.5Minimal aerobic benefitMinimal anaerobic stress

Analytics block ​

Requires streams to be fetched. Contains HR and pace time-in-zone breakdowns:

json
"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):

json
"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 ​

json
"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:

bash
fitops activities set-race-result 17985851162 --chip-time 40:30 --race-distance-km 10
fitops activities clear-race-result 17985851162

laps ​

json
"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 ​

json
"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:

json
"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:

json
"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.

bash
fitops activities stamp [OPTIONS]

Options:

FlagDefaultDescription
--id STRAVA_ID—Stamp a single activity by Strava ID
--local-id ACTIVITY_ID—Stamp an imported activity locally by FitOps ID
--allfalseStamp all activities that haven't been stamped yet
--forcefalseRe-stamp even if already stamped (fetches fresh description from Strava)
--jsonfalseOutput 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:

json
{
  "stamped": [12345678, 12345679],
  "skipped": [],
  "failed": []
}
  • stamped — Strava IDs of activities successfully updated
  • skipped — 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:

bash
# 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 --json

The 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 ​

← Commands Reference

FitOps CLI — terminal-first training analytics