Skip to content

Mapping — FIT

Source: a FIT file (Garmin/ANT+ Flexible and Interoperable Data Transfer — the format behind Garmin, Wahoo, Zwift, and TrainingPeaks exports). Pillars: A + B. Mapper: mapFitmapFit(decoded) → { records, warnings }.

FIT files carry two structurally different things, and the mapper branches on which is present:

A. Recorded activity → telemetry + a performed Session

session / record messages, structurally the same shape as the Strava mapping: per-sample streams become Pillar A Measurements, and the activity becomes a Pillar B Session that references them via measuredBy (§1.3).

FITOpenBody
records[].heart_rate / power / cadencesingle-channel sampleArray Measurement (heart_rate /min, power W, cadence /min)
records[].position_lat / position_long / altitudeone multi-channel location sampleArray (channels lat/lon/alt) — semicircle→degree conversion is the decoder’s job, not the mapper’s
records[].timestampthe shared offsets array (seconds from the first record)
sessions[0].total_distance / total_elapsed_time / total_caloriesthe Session’s continuous WorkUnit.performance
sessions[0].sportdisciplines (known FIT sports map directly; unrecognized ones round-trip as fit:<sport>)
// Output (abridged) — a heart-rate stream + the Session that references it
{ "id": "fit-hr", "recordType": "Measurement", "type": "heart_rate", "unit": "/min",
"sampleArray": { "offsets": [0, 60, 120, 180, 240], "dataPoints": [138, 149, 156, 160, 168] },
"startTime": "2026-07-02T06:30:00Z", "endTime": "2026-07-02T06:34:00Z",
"provenance": { "method": "sensor", "sourceApp": "fit" } }
{ "id": "fit-session", "recordType": "Session", "disciplines": ["running"], "intent": "train",
"startTime": "2026-07-02T06:30:00Z", "endTime": "2026-07-02T06:34:00Z",
"links": [{ "type": "measuredBy", "ref": "fit-hr" }, ""],
"workUnits": [{ "id": "fit-session-wu", "recordType": "WorkUnit", "scoring": "continuous",
"performance": { "distance": { "absolute": { "value": 800, "unit": "m" } },
"time": { "absolute": { "value": 240, "unit": "s" } },
"energy": { "absolute": { "value": 62, "unit": "kcal" } } } }] }

B. Structured workout → a planned Session of prescriptions

workout / workout_step messages — a planned interval workout (the kind Zwift, TrainingPeaks, or Garmin Connect’s workout builder export), not a recorded activity. This is new modeling territory: every other mapper here maps performed data; this is the first that maps FIT’s prescription side onto Program/Block/WorkUnit/Target (§5).

FIT workout_stepOpenBody
duration_type: time|distance|reps|calories + duration_valueWorkUnit.scoring + the matching prescription metric (time/distance/reps/energy)
duration_type: open (no auto-bound; athlete advances manually)prescription.time = { stopCondition: { kind: "open" } }
duration_type: repeat_until_steps_cmplt, duration_value (loop-back index), target_value (count)a Block with repetitions: N wrapping the referenced step range — a repeat-control step, not a real training step
target_type: power|heart_rate|speed|cadence|grade + target_valueprescription.intensity[]dimension per §5.13 (hr/speed/power/grade; cadence is an open-vocab extension of the same mechanism), value: { absolute: {…} }
custom_target_value_low/_high on a non-edge stepvalue: { range: { min, max } }
custom_target_value_low/_high on a warmup/cooldown stepvalue: { ramp: { from, to } } — directional, per SPEC.md’s own Zwift Warmup/Cooldown/Ramp crosswalk precedent
intensity: warmup|activeWorkUnit.setRole: warmup|working (registry-backed open vocab, §5.5); anything else (rest/cooldown/recovery/interval/other) round-trips source-namespaced (fit:rest, …), same pattern as the Apple Health mapper’s unmapped-token fallback
wkt_step_namethe wrapping Block.nameWorkUnit itself has no name field
// Input (excerpt) — a warmup, a 4x interval/rest repeat, a cooldown
{ "workouts": [{ "wkt_name": "Sweet Spot Intervals", "sport": "cycling" }],
"workout_steps": [
{ "message_index": { "value": 0 }, "wkt_step_name": "Warm Up", "duration_type": "time", "duration_value": 600,
"target_type": "power", "custom_target_value_low": 100, "custom_target_value_high": 150, "intensity": "warmup" },
{ "message_index": { "value": 1 }, "wkt_step_name": "Interval", "duration_type": "time", "duration_value": 300,
"target_type": "power", "target_value": 200, "intensity": "active" },
{ "message_index": { "value": 2 }, "wkt_step_name": "Recover", "duration_type": "time", "duration_value": 120,
"target_type": "open", "intensity": "rest" },
{ "message_index": { "value": 3 }, "duration_type": "repeat_until_steps_cmplt", "duration_value": 1, "target_value": 4 },
{ "message_index": { "value": 4 }, "wkt_step_name": "Cool Down", "duration_type": "time", "duration_value": 600,
"target_type": "power", "custom_target_value_low": 80, "custom_target_value_high": 150, "intensity": "cooldown" }
] }
// Output (abridged) — Session.blocks: [warmup-Block, 4x-repeat-Block, cooldown-Block]
{ "id": "fit-wkt", "recordType": "Session", "name": "Sweet Spot Intervals",
"disciplines": ["cycling"], "intent": "train",
"blocks": [
{ "id": "fit-wkt-blk-0", "recordType": "Block", "name": "Warm Up",
"children": [{ "recordType": "WorkUnit", "setRole": "warmup", "scoring": "time",
"prescription": { "time": { "absolute": { "value": 600, "unit": "s" } },
"intensity": [{ "dimension": "power", "unit": "W", "value": { "ramp": { "from": 100, "to": 150 } } }] } }] },
{ "id": "fit-wkt-blk-3", "recordType": "Block", "repetitions": 4,
"children": [
{ "recordType": "Block", "name": "Interval", "children": ["", "setRole: working, target 200 W"] },
{ "recordType": "Block", "name": "Recover", "children": ["", "no target — duration_type: open"] }
] },
{ "id": "fit-wkt-blk-4", "recordType": "Block", "name": "Cool Down", "children": ["", "ramp 150→80 W"] }
] }