Mapping — Strong
Source: Strong app CSV export (one row per set; , or ; delimited). Pillar: B
(strength). Mapper:
mapStrong —
mapStrong(csv) → { records, warnings }, one Session per workout.
Structural correspondence
| Strong CSV | OpenBody |
|---|---|
a workout (rows sharing Date + Workout Name) | Session (disciplines: ["strength"], name ← Workout Name, clientRecordId ← the natural key) |
consecutive rows for one Exercise Name | Exercise (exerciseRef: canonical registry id where the crosswalk resolves the name, plus the lossless opaque original) |
| one set row | WorkUnit (scoring from Reps/Distance/Seconds) |
Weight | performance.load (unit: "kg", basis: "marked_weight") |
Reps / Distance / Seconds | performance.reps / .distance (m) / .time (s) |
Notes | WorkUnit.notes |
Workout No | Session.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 No2025-12-20 18:00:00,Push Day,3600,Bench Press (Barbell),1,80,5,0,0,,12025-12-20 18:00:00,Push Day,3600,Plank,1,0,0,0,60,core hold,1Output (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) becomesclientRecordId(§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) setsendTime = startTime + Duration.- Timestamps. Strong’s
Dateis wall-clock with no offset; the mapper stamps it with the optionalutcOffsetoption (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
Plankrow (Seconds: 60) becomes atime-scoredWorkUnit; a distance row becomesdistance-scored. - Exercise identity resolves to canonical registry ids where the crosswalk covers the name
(both names above resolve), keeping the
opaqueoriginal alongside — like Hevy. - The mirror direction exists too:
mapOpenBodyToStrongwrites this same CSV shape back out.