Skip to content

Mapping — Strong

Source: Strong app CSV export (one row per set; , or ; delimited). Pillar: B (strength). Mapper: mapStrongmapStrong(csv) → { records, warnings }, one Session per workout.

Structural correspondence

Strong CSVOpenBody
a workout (rows sharing Date + Workout Name)Session (disciplines: ["strength"], nameWorkout Name, clientRecordId ← the natural key)
consecutive rows for one Exercise NameExercise (exerciseRef: canonical registry id where the crosswalk resolves the name, plus the lossless opaque original)
one set rowWorkUnit (scoring from Reps/Distance/Seconds)
Weightperformance.load (unit: "kg", basis: "marked_weight")
Reps / Distance / Secondsperformance.reps / .distance (m) / .time (s)
NotesWorkUnit.notes
Workout NoSession.extension["io.strong.export"].workoutNo

Input (two exercises, one of them a plank)

Date,Workout Name,Duration,Exercise Name,Set Order,Weight,Reps,Distance,Seconds,Notes,Workout No
2025-12-20 18:00:00,Push Day,3600,Bench Press (Barbell),1,80,5,0,0,,1
2025-12-20 18:00:00,Push Day,3600,Plank,1,0,0,0,60,core hold,1

Output (OpenBody wire record)

{
"id": "strong-w-87ab88ae",
"recordType": "Session",
"subject": "subj-001",
"clientRecordId": "2025-12-20 18:00:00|Push Day",
"disciplines": ["strength"],
"startTime": "2025-12-20T18:00:00Z",
"endTime": "2025-12-20T19:00:00Z",
"name": "Push Day",
"extension": { "io.strong.export": { "workoutNo": "1" } },
"exercises": [
{
"id": "strong-w-87ab88ae-ex0",
"recordType": "Exercise",
"exerciseRef": { "id": "bench-press.barbell.flat", "opaque": "Bench Press (Barbell)" },
"workUnits": [
{
"id": "strong-w-87ab88ae-ex0-set0",
"recordType": "WorkUnit",
"scoring": "reps",
"performance": { "reps": 5, "load": { "value": 80, "unit": "kg", "basis": "marked_weight" } }
}
]
},
{
"id": "strong-w-87ab88ae-ex1",
"recordType": "Exercise",
"exerciseRef": { "id": "plank", "opaque": "Plank" },
"workUnits": [
{
"id": "strong-w-87ab88ae-ex1-set0",
"recordType": "WorkUnit",
"scoring": "time",
"performance": { "time": 60 },
"notes": "core hold"
}
]
}
]
}

Notes

  • Stable ids. The export has no workout id of its own, so the natural key (Date|Workout Name) becomes clientRecordId (§7.1) and a content hash of it the record id — positional numbering would renumber everything when one more workout is exported, defeating dedup.
  • Duration (seconds) sets endTime = startTime + Duration.
  • Timestamps. Strong’s Date is wall-clock with no offset; the mapper stamps it with the optional utcOffset option (default "Z"), never the machine’s local timezone — the same bytes map to the same records everywhere.
  • App-specific residue (Workout No) has no canonical home, so it goes to a namespaced extension (io.strong.export) rather than being dropped — the canonical-plus-residue principle (§10.1).
  • Mixed scoring per exercise works: a Plank row (Seconds: 60) becomes a time-scored WorkUnit; a distance row becomes distance-scored.
  • Exercise identity resolves to canonical registry ids where the crosswalk covers the name (both names above resolve), keeping the opaque original alongside — like Hevy.
  • The mirror direction exists too: mapOpenBodyToStrong writes this same CSV shape back out.