Bench Press · RPE 7.5 · 06:14
It’s yours.
No asterisks.
Most fitness apps gate export behind a Pro tier, a CSV with three columns, or a JSON dump you can’t actually read. flexRep is built on the opposite stance: data portability is a product pillar, not a checkbox feature. Export is free now and forever.
Every set, every field.
One row per set. ISO timestamps. Weight in kilograms — the canonical unit. RPE half-steps. The notes field carries whatever you typed in the app. The columns below are the core ones; the full export carries 40+ fields, including gym, set-type parent/child, tempo, climbing grade, and source.
Full schema. Plain shape.
Workouts → ExercisePerformances → SetEntries. UUIDs everywhere. Soft deletes filtered out. Export metadata at the top — version, timestamp, unit. (The verifiable SHA-256 checksum rides on the .flexrep backup bundle below.)
The same schema docs/SCHEMA.md publishes for developers. No mystery fields. Nothing renamed for “branding”. If you import a flexRep export into a spreadsheet six years from now, every field will still make sense.
{
"version": "0.1.0",
"exportedAt": "2026-05-19T06:48:12Z",
"unit": "kg",
"workouts": [
{
"id": "F7C2E9B4-…",
"startedAt": "2026-05-19T06:00:00Z",
"endedAt": "2026-05-19T07:08:23Z",
"rating": 5,
"place": "Iron & Oak — Home Gym",
"tags": ["heavy", "bench"],
"performances": [
{
"id": "…",
"exercise": "Bench Press",
"ordinal": 0,
"sets": [
{
"id": "…",
"weightKg": 102.0586,
"reps": 5,
"rpe": 7.5,
"rir": 2.5,
"setType": "working",
"createdAt": "2026-05-19T06:14:03Z"
}
]
}
]
}
]
} Where every logged set actually lives.
One tap of Log writes to four places at once. None of them depend on a flexRep server, because there isn't one.
Atomic write · same identifier across all four destinations · deletions propagate · no flexRep server in the loop.
Sets are first-class citizens. Definitions live elsewhere.
The shape of your training data shouldn't change because we renamed a button. The model is built so that every set you've ever logged survives every iteration of the app that comes after.
The relationships, in plain English.
Exercise (the library)
↑
│ referenced by many — never cascades
│
Workout ──▶ ExercisePerformance ──▶ Set
(session) (this workout's bench) (weight × reps × effort)
│
└─▶ Gym (optional) Delete a workout, and its performances and sets go with it — that’s intentional. Delete an exercise from the library or archive a gym, and your historical logs stay untouched. The shape of the graph is the shape of how training data actually behaves.
Principles we don’t bend.
- Library metadata lives in one place. Renaming an exercise touches one row, not a thousand log entries.
- Sets are their own things. Each one has its own identifier, its own effort, its own notes — never encoded into a string on a parent row.
- Every record gets a globally-unique identifier and timestamps from day one. Cross-device sync depends on it; debugging benefits from it.
- Canonical units stored, converted only at the edges. Switching display units never mutates your data.
- Categorical data is human-readable. Set type and movement pattern are stored as words, not magic integers.
- Every entity has escape hatches. Tags and an opaque metadata field absorb feature requests without forcing a migration.
- Versioning is a v1 problem. The schema is versioned from the start, so v2’s first migration doesn’t require an emergency.
The number under your finger is not the number on disk.
Weight is stored in kilograms at full floating-point precision, regardless of what your display shows. Not because we believe in micro-gram accuracy. Because we believe in not losing data in conversion.
Canonical: kilograms, at full precision.
225 lb displayed → 102.06 kg stored. On reload, kg → display unit, rounding only at the view layer. A 225 lb user who switches to kg sees 102.06 kg. A 102.06 kg user who switches to lb sees 225 lb. The math is reversible because the storage is canonical.
Half-step scale. RIR derived from RPE.
Half-steps are coarse enough to be honest. Tenths would imply discrimination most lifters can’t actually make. We store both RPE and RIR so the user can think in whichever framing they prefer.
Always stored as seconds.
Aggregating "13 min + 1:42:18" across a year of sessions is the kind of thing that produces off-by-three-hours bugs forever. Seconds compose.
Storage: UTC. Display: user’s locale at the moment of display.
Train in Tokyo, fly to LA, train again — the gap between sessions is unambiguous because both timestamps are UTC. The display layer figures out which one is "yesterday."
Your data is never one crash away from gone.
Most data-loss stories aren't dramatic. A phone dies mid-rest. The app crashes between sets. A sync conflict silently overwrites yesterday's PR. Here's the architecture that prevents each one.
Saves happen at the event, not on a timer.
The moment you tap Log, the set is on disk before the animation finishes. No periodic-flush window. No "lost the last 30 seconds" scenarios. Every state change — start, log, edit, rest start, rest end, exercise switch — is its own persisted event.
Crash recovery is structural, not heroic.
Open the app after a crash and the workout is intact. The pause state is intact. The rest timer remembers when it should end (stored as an absolute moment in time, not a countdown). Every set is exactly where you left it.
The atomic unit is one set.
Each set is its own record with its own identifier and timestamps. A crash mid-write loses, at worst, the single set you were in the middle of typing — never the workout structure, never the sets around it, never the session.
No silent failures.
Every persistence write is wrapped. If a write fails, the app surfaces a global alert so you can resolve it on the spot. We never quietly swallow an error. The alert is annoying. The corrupted log it prevents is much worse.
Reads are scoped.
No view loads an unbounded history. Year-spanning aggregations happen off the main thread with cached results. The logging surface stays instant even after years of training data.
Sync conflicts are arbitrated, not overwritten.
When two devices touch the same record, conflict resolution happens at the field level using timestamps — not last-write-wins at the row level. Editing weight on the Watch never wipes a note you typed on the phone.
What 'data ownership' actually means here.
Free, forever.
CSV and JSON export are in the free tier. No subscription. No "Pro export" upsell. If you can log a set, you can pull every set.
Every column, every set.
No "summary mode." No "Pro adds RPE column." Every row carries weight, reps, RPE, RIR, set type, equipment, group, notes, timestamp.
No vendor lock-in.
Generic CSV. Plain JSON. Signed backup bundles with a checksum. Any other tool can read your data — including the ones that don’t exist yet.
Reversible imports.
Every import tags rows with a shared batch identifier. Revert removes only that batch; your native data is untouched.
Two-way HealthKit.
flexRep writes per-set metadata to Apple Health. Body weight syncs both ways. If you delete the app, your data already lives in Health.
On-device by default.
No analytics SDKs, no ad networks, no telemetry. Foundation Models runs locally. iCloud sync is opt-in.
A clean library. One bench press.
Or: how we keep one canonical 'Bench Press' across hundreds of curated exercises, four typing styles, and one user who insists on logging 'Benchpress (Flat)' with no space.
Flat rows, grouped by attribute.
“Incline Dumbbell Bench Press” is its own row. Not a child of “Bench Press” — its own entry, tagged with shared attributes so analytics still roll up correctly.
Users search and log by fully-qualified name. Grouping and substitution happen at the query layer, not the schema.
Normalized aliases.
Every exercise carries a list of aliases. They get lowercased, diacritics stripped, and normalized into a single canonical form. Search matches against the normalized version, so spelling drift doesn’t fracture the library.
| “Bench Press” | → | one record |
| “BB Bench” | → | same record |
| “Flat Bench” | → | same record |
| “Benchpress” | → | same record |
| “flat barbell press” | → | same record |
Provenance, preserved.
Every exercise in the library carries its origin and the seed version it shipped in. On updates, we match by stable identifier — never by name — and preserve any user edits.
New entries insert cleanly. Removed entries archive instead of deleting. Your logs never lose their target.
Movement-pattern inference.
Curated exercises carry hand-tagged movement patterns. User-created and imported exercises get a pattern inferred from name and primary muscle.
Inference is high accuracy, not perfect. Anywhere it gets a call wrong, you can correct it — and your correction is sticky across seed updates.
Importing from Strong, Hevy, or anywhere.
The hardest part of switching apps is the data. Most 'switch to ours' promises require you to lose history. flexRep doesn't — and the path from another app to a fully-functional flexRep library is six steps, reversible at every one.
- 01
Pick your source
Strong, Hevy, generic CSV from anywhere, or a previous .flexrep backup. Files come from your local storage or a share-sheet hand-off — never a third-party server.
- 02
Auto-map exercises
flexRep fuzzy-matches every exercise name in your file against its curated library. Aliases like "BB Bench," "Flat Bench," and "Benchpress" collapse to the same canonical entry. Anything unmapped is yours to assign manually.
- 03
Preview before commit
See the first several rows exactly as they’ll land — including mapped exercise names, RPE, set type, and any inferred fields. Nothing is written to your library yet.
- 04
Commit as a batch
Every row from a single import inherits a shared batch identifier. Your existing logs are never touched. The batch lives alongside your native data, fully usable in analytics — and fully separable.
- 05
Use it immediately
Analytics, charts, exercise history, e1RM curves — all include imported sets the second the batch commits. There’s no "reindexing" pause.
- 06
Revert if anything’s off
Every batch is listed in Your Training Data with a one-tap revert. Reverting removes only that batch’s rows; everything you logged natively is left alone, untouched.
What you can count on.
If you import the same CSV twice — by accident, or because the first run partially failed — duplicate detection skips rows that already exist. Worst case: nothing happens.
Imported sets carry a tag for the source app and the import batch. Analytics treat them as equals to native sets, but the source is queryable. You can always ask "which of these came from Hevy?"
A small log records every import: when it happened, how many rows, what source. Useful when you’re six months in and trying to remember what you brought over.
If an import fails halfway through, the entire batch is rolled back. You never end up with half a year of squat data and zero bench.
Free export is not the destination. It's the starting line.
Once you have a clean CSV and a readable JSON in your hands, every modern analysis tool will read them — including a category that didn't exist a few years ago.
Classical tools
The 11-column CSV and the readable JSON drop into every analysis surface you already use.
- Excel · Sheets · NumbersspreadsheetsPivot tables by exercise, conditional formatting for PRs, your own e1RM formulas in the cells next to the canonical ones.
- Python · pandas · matplotlibnotebooksRead the CSV in two lines. Build your own stall detector. Plot volume against bodyweight. Run anything the app analytics don’t.
- R · tidyversestatisticsRegress strength velocity against rest intervals. Test your own hypothesis. Render publication-quality charts.
- Notion · Obsidian · Roamsecond brainPipe the JSON into your personal training journal. Link sets to daily notes. Build dashboards in your second brain.
- Tableau · Looker StudiodashboardsA multi-year strength dashboard you actually own. Drop the CSV into a Google Drive folder, point Looker Studio at it, and you have a free live dashboard.
- iOS ShortcutsautomationAuto-pipe weekly exports into Drive, Dropbox, or email. Run a daily summary into Apple Notes. The export Shortcut action is callable from anywhere.
AI tools
Drop the JSON into a chat with any modern LLM. Ask the questions you’d ask a thoughtful coach. The model sees every set you’ve ever logged, at the granularity you logged it.
“Here is my last six months of training data as JSON. Identify any exercises where my e1RM has been flat for three weeks or more despite stable RPE. Suggest which ones look like programming stalls vs. life-stress drops. Be conservative — only flag what is unambiguous.”
“Using the attached JSON, design a four-week strength block followed by a deload, focused on bench press and squat. Anchor weights to my recent working-RPE-8 sets, not my e1RM. Output the program as a markdown table with weight, reps, and RPE targets per session.”
“My workouts are tagged with the gym they happened at. Compare my squat performance at "Home" vs. "Commercial" gyms across the last 90 days. Are there meaningful differences? If yes, hypothesize why.”
“Cluster my sessions by intensity profile (RPE distribution + e1RM relative to recent max). Which clusters appear most often in the two weeks before a PR? Which clusters most often precede a missed set?”
“Based on the attached week of training data, write a short journal entry in the voice of a thoughtful intermediate lifter. Mention what felt good, what felt off, and one specific thing to try next week. Three paragraphs, calm tone, no hype.”
“I am sending this JSON to a new coach. Write a one-page summary of my last twelve weeks: training frequency, exercise selection, working-rep ranges, average RPE per lift, recent PRs, and any concerning patterns. Markdown formatted.”
We didn’t ship an LLM ourselves. We didn’t have to: the readable JSON unlocked the whole category for free. Your data, your model of choice, your prompts.
Pull your data. Anytime.
If we ever paywall export, it stops being our product.