Skip to content
Your data

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.

The CSV

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.

Column
Example
Notes
Date
2026-05-19
ISO 8601
Time
06:14:03
Local time of the set
Exercise
Bench Press
Canonical exercise name
Group
Chest
Primary muscle group
Equipment
Barbell
Equipment used
Set Type
working
warmup, working, drop, etc.
Weight
102.05
Canonical kilograms
Reps
5
Integer
RPE
7.5
Half-step scale
RIR
2.5
Derived from RPE
Notes
"top set; bar speed clean"
Free-form, CSV-escaped
The JSON

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"
            }
          ]
        }
      ]
    }
  ]
}
One set, four destinations

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.

SOURCE
185
×
5

Bench Press · RPE 7.5 · 06:14

On-device store
Stable identifier + timestamps
CSV row
40+ columns · scripts & sheets
JSON node
full schema · readable shape
Apple Health
per-set workout segments

Atomic write · same identifier across all four destinations · deletions propagate · no flexRep server in the loop.

The data model

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.

  1. Library metadata lives in one place. Renaming an exercise touches one row, not a thousand log entries.
  2. 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.
  3. Every record gets a globally-unique identifier and timestamps from day one. Cross-device sync depends on it; debugging benefits from it.
  4. Canonical units stored, converted only at the edges. Switching display units never mutates your data.
  5. Categorical data is human-readable. Set type and movement pattern are stored as words, not magic integers.
  6. Every entity has escape hatches. Tags and an opaque metadata field absorb feature requests without forcing a migration.
  7. Versioning is a v1 problem. The schema is versioned from the start, so v2’s first migration doesn’t require an emergency.
Units & precision

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.

WEIGHT
102.0586

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.

RPE / RIR
7.5

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.

DURATION
14,820

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.

TIMESTAMPS
UTC · ISO 8601

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

The write path

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.

Six promises

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.

Cleanliness

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.

Bring it over

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.

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

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

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

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

  5. 05

    Use it immediately

    Analytics, charts, exercise history, e1RM curves — all include imported sets the second the batch commits. There’s no "reindexing" pause.

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

Idempotent re-imports

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.

Provenance preserved

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?"

Audit trail

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.

No partial imports left dangling

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.

SUPPORTED SOURCES
Strong CSV Hevy CSV Generic CSV any app .flexrep backup native Apple Health per-set
Where your data can go

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.

SINCE FOREVER

Classical tools

6 destinations

The 11-column CSV and the readable JSON drop into every analysis surface you already use.

  • Excel · Sheets · Numbers
    spreadsheets
    Pivot tables by exercise, conditional formatting for PRs, your own e1RM formulas in the cells next to the canonical ones.
  • Python · pandas · matplotlib
    notebooks
    Read the CSV in two lines. Build your own stall detector. Plot volume against bodyweight. Run anything the app analytics don’t.
  • R · tidyverse
    statistics
    Regress strength velocity against rest intervals. Test your own hypothesis. Render publication-quality charts.
  • Notion · Obsidian · Roam
    second brain
    Pipe the JSON into your personal training journal. Link sets to daily notes. Build dashboards in your second brain.
  • Tableau · Looker Studio
    dashboards
    A 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 Shortcuts
    automation
    Auto-pipe weekly exports into Drive, Dropbox, or email. Run a daily summary into Apple Notes. The export Shortcut action is callable from anywhere.
SINCE LAST YEAR

AI tools

NEW LANE

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.

BRING YOUR OWN MODEL
Claude ChatGPT Gemini Local
Stall detection — on your terms

“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.”

Periodization, drafted

“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.”

Place-aware analysis

“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.”

Pattern mining

“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?”

The training journal you never write

“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.”

Coach handoff

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