The .badger file format

A .badger file is how Badger hands a routine, a gym, or a set of machines to somebody else. It is a single UTF-8 JSON file. Nothing is compressed, encoded, or encrypted: open it in any text editor and you can read the whole thing.

This page documents it properly because three groups of people want it. Curious users, who would rather see what they are importing before they import it. Tool authors, who want to write something that emits a file Badger can read. And AI assistants: point yours at this page, hand it the example, and ask it to write you a program as a valid .badger file. The BYO-AI guide has prompts for exactly that, and the program gallery has seven real files to read.

Tip

A file is just a plan. It carries routines, exercise names, and equipment settings. It never carries your logged workouts, your history, or anything about you.

The envelope

Every file, whatever it holds, has the same five top-level fields.

FieldTypeMeaning
badger string Schema version. Today it is always share/v1. A build imports the versions it knows; anything else is refused rather than guessed at.
app_version string The app version that wrote the file, for example 1.1.1. Informational.
created_at string UTC timestamp in ISO 8601. Informational.
type string One of routine, gym, machines. Any other value and the file is rejected as invalid.
payload object The contents. Its shape depends on type: a routine payload or a gym or machines payload.

Optional fields are omitted rather than written as null, and unknown fields are ignored on the way in. Writing a file by hand is therefore forgiving: include what you mean, leave out what you do not.

A routine payload

type: "routine". The payload has four keys:

KeyTypeContents
routineRoutineThe routine itself: name, notes, and its settings.
daysarray of DayThe days, in order. Each day carries its own exercises array: that day's slots, in order, with their planned sets.
exercisesarray of ExerciseThe definition of every exercise any day references: its name, type, and category. Day slots point into this list by exercise_name.
machinesarray of MachineMachines the day slots pin. Always written; the import preview asks before creating any.

Note that two arrays share the name exercises and they hold different types: exercises at the top level is an array of Exercise, defining what each exercise is, while a day's exercises is an array of DayExercise, saying what that day does and referencing the definitions by name. Every name a day uses should have a definition in the top-level list - a slot whose name matches no definition is skipped - the import preview counts these and warns you before anything is written.

Routine

The object in the payload's routine key.

FieldTypeMeaning
namestring, requiredThe routine name.
notes string Program-level notes. Carried, because they describe the program.
weight_source string Where starting weights come from: last_performance (default), routine_defaults, or routine_defaults_then_last.
history_scope string Which history the weight lookup reads: any_workout (default) or this_routine.

Day

The objects in days.

An array. One entry per session in the program.

FieldTypeMeaning
namestring, requiredDay name, for example "Push" or "Workout A".
scheduled_days array of integers Weekdays this day is scheduled on: [1, 3, 5] means Monday, Wednesday, Friday. The numbering is fixed: 1 is always Monday and 7 is always Sunday, counting from 1 rather than 0, and it does not change with the app's first-day-of-week setting or where in the world the file is opened. Values outside 1-7 are dropped on import. Leave the field out for a program you rotate through rather than pin to weekdays.
default_gym_name string Gym this day defaults to. The gym is created on import if the name is new.
exercisesarray of DayExerciseThe day's exercise slots.

DayExercise

The objects in a Day's exercises array: one slot in the plan.

FieldTypeMeaning
exercise_name string, required Must match a name in the payload's exercises list.
exercise_type string, same values as Exercise.type The referenced exercise's type, repeated here. Optional, but include it: it is what keeps a "Plank" you time apart from a "Plank" someone counts reps on. When present it must exactly equal the referenced definition's type - import resolves the slot by the name and type together, and a mismatch means the slot matches nothing - counted in the import preview's warning and skipped, like an unmatched name.
rest_override_seconds integer Rest for this slot, when it should differ from the exercise's own default.
default_machine_name string Machine this slot is pinned to, by name. Resolved against the reader's own machines and the payload's machines list together, so a machine the recipient already has does not need to travel in the file. Include it in machines when the file should also work for someone who lacks it; the name matching nothing just leaves the slot unpinned. Import links the matched machine to the exercise.
superset_key string Slots in the same day sharing a key become a superset, in the order they appear. The key itself is opaque - "1", "a", anything - it only has to match between the members. A key held by a single slot is ignored.
superset_name string Display name for the superset. The first member's value wins; leave it out and the app names the group itself. Group color never travels - the recipient's own palette assigns it.
tempo string Tempo prescription for the slot, carried word for word, for example "3-1-2-0".
alternatives array Pre-assigned swap options for this slot, in order: objects of the form { "exercise_name": "...", "exercise_type": "..." }, resolved by the same rules as the slot itself, so each one needs a definition in the payload's exercises list. An unmatched alternative is counted in the import preview's warning and skipped.
predefined_sets array of PredefinedSet The planned sets. An empty list is valid but pointless.

PredefinedSet

The objects in a DayExercise's predefined_sets.

A planned set. Fill in the fields the exercise type actually uses: weight and reps for a bench press, duration_seconds for a plank, distance for a row. Fields that do not apply are simply left out.

FieldTypeMeaning
is_warmup boolean, defaults to false Defaults to false. Warmup sets do not count toward your working volume.
weight number A planned weight, in the unit named by weight_unit. Most shared programs leave this out and let each lifter's own weights fill in.
weight_unit string "kg" or "lbs". Weights are stored in the unit they were entered in, never converted. Absent means kg.
reps integer The rep target. With reps_max present it is the bottom of a range instead.
reps_max integer Top of a rep range, so reps: 8, reps_max: 10 reads as 8-10. Leave it out for an exact target. Never valid on a warmup: import drops it there.
is_amrap boolean, defaults to false An open-topped target: reps becomes a required minimum with no ceiling, shown as 8+, and reps: 1, is_amrap: true is how a set to failure is written. Mutually exclusive with reps_max, and if a file somehow carries both, open wins and the range is dropped. Never valid on a warmup. Omitted when false, so a program with no open targets is byte-identical to one written before the key existed - and because reps still holds the floor, an older reader sees a sane exact target rather than nothing.
duration_secondsintegerPlanned time, for timed exercises.
distancenumberPlanned distance in kilometers.

Exercise

The objects in the payload's top-level exercises: the definitions the day slots reference.

The definitions the days point at. Keep it to the exercises the routine actually uses; a recipient's existing library is matched first, so most of this list usually resolves to exercises they already have.

FieldTypeMeaning
name string, required Matching is by name, so ordinary names travel best. "Barbell Back Squat" lands on the recipient's existing squat; "BB Squat (heavy day)" creates a new exercise.
type string One of weight_reps (the default), reps_only, time_only, distance_only, weight_only, weight_time, weight_distance, reps_time, reps_distance, distance_time. An unrecognized value falls back to weight_reps.
category_name string Category for an exercise that has to be created. Any name is valid: an existing category is reused by name, a new name creates one, and absent reads as "Other". A fresh install ships Chest, Back, Shoulders, Biceps, Triceps, Legs, Core, and Cardio - not an enum, since categories can be renamed or deleted, but using these lands new exercises where most libraries already sort them.

Machine

The objects in machines, here and in a gym payload alike.

Equipment settings. In a routine payload this list is reference data: importing the machines is a toggle the recipient controls. In a gym or machines payload the same object is the point of the file, and machines are always created.

FieldTypeMeaning
namestring, requiredMachine name.
notesstringFree text about the machine - "third from the left, seat pin sticks". Notes about the machine itself travel; the private setup cues you attach to one exercise on it do not.
uses_attachmentsboolean, defaults to falseWhether the machine takes attachments (bars, handles, ropes).
uses_grip_typesboolean, defaults to falseWhether grip is worth recording on this machine.
supports_bands boolean, defaults to false Whether bands can be attached here. It is what decides that a machine exercise offers accessories at all, and only band-kind ones. Omitted when false, which is every machine that has not been marked.
machine_type string "selectorized" (a stack with a pin) or "plate_loaded". Older files with "plateLoaded" still read.
weight_increment number Smallest real step on this machine, in the machine's own unit.
weight_increment2 number Second step size, for a stack whose plates get heavier further up.
increment_threshold number Weight at and above which weight_increment2 applies.
plate_calculator_mode string "dual_loaded" (plates on both sides) or "single_loaded" (one lever arm). Older files with the camelCase values still read.
bar_weight number Weight of the bar or empty carriage, in the unit named beside it - the same value-plus-unit pattern set weights use, so a 45 lb bar travels as 45.
bar_weight_unit string "kg" or "lbs", for bar_weight. Older files carry bar_weight_kg instead, always in kg; it is still read.
weight_unit string "kg" or "lbs", when this machine is labeled in a different unit from the rest of your logging.
logs_weight_per_side boolean What a weight logged on this machine means: true for one side (iso-lateral machines), false for the total. Leave the key out entirely if the machine has no opinion - the reader then falls back to the exercise's own setting, which is not the same as writing false. Only meaningful on a plate-loaded machine; a value on any other kind is ignored. Older readers ignore the key and lose nothing else.

Gym and machines payloads

type: "gym" shares a whole gym: the gym itself, its machines, its attachments, its bars and accessories, which of each belongs to it, and the equipment that is a list rather than an object - the dumbbell and kettlebell racks, and the plates.

The line it draws is worth stating plainly, because it decides every field below. What describes the gym travels; what describes you does not. Notes about a machine describe the machine, so they go. Your photo of it, and the setup cues you saved against one exercise on it, describe you, so they stay.

Definitions and membership are separate on purpose, mirroring the app: machines and attachments are global there, and a gym holds links to them, so one machine can belong to several gyms. In a file Badger writes, the membership lists always name exactly the objects defined beside them - the split is structure, not extra information to fill in.

FieldTypeMeaning
gymGymA Gym is just { "name": "..." }.
machinesarray of MachineThe same Machine objects a routine payload carries.
attachmentsarray of AttachmentAn Attachment is a name and optional notes.
barsarray of BarNamed bars. Omitted entirely when the gym has none, so a bar-free file looks exactly like one written before bars existed.
accessoriesarray of AccessoryBands, chains, and straps. Omitted entirely when the gym has none.
gym_machinesarray of stringsNames of the machines that belong to this gym.
gym_attachmentsarray of stringsNames of the attachments that belong to this gym.
gym_barsarray of stringsNames of the bars that belong to this gym.
gym_accessoriesarray of stringsNames of the accessories that belong to this gym.
racksarray of RackThe gym's dumbbell and kettlebell racks. Omitted entirely when the gym has none set up.
plate_inventoryarray of PlateInventoryThe plates the gym actually has. Omitted entirely when nobody has said.

All four membership lists follow the same rule, which matters if you are writing a file by hand: leaving one out means "all of them", and writing it as [] means "none of them". Those are different, and the second one is almost never what you meant - a gym whose machines belong to no gym is a gym with nothing in it. Say nothing and everything you defined lands at the gym, which is the obvious reading of a gym file and therefore the one the reader takes.

Bar

The objects in bars: a named bar, with its own weight.

FieldTypeMeaning
namestring, requiredBar name, for example "Olympic bar" or "Trap bar".
notesstringFree text about the bar.
bar_weight number What the bar weighs empty, in the unit named beside it - the same value-plus-unit pattern set weights and machine bar weights use, so a 45 lb bar travels as 45. Leave it out for a bar nobody has weighed: the reader then falls back to the machine's or the exercise's own bar weight rather than blanking it.
bar_weight_unit string "kg" or "lbs", for bar_weight. Absent means kg.

A routine payload deliberately carries no default bar on a slot, the same as default attachment and default grip: which bar somebody reaches for is their setup, not your program.

Accessory

The objects in accessories: a band, a chain, a set of suspension straps, or an add-on that is none of those.

FieldTypeMeaning
namestring, requiredAccessory name, for example "Green band".
kind string, required One of band, chain, suspension, other. An unrecognized value reads as other, which is exactly what it means: an add-on whose kind this build has no word for.
color integer Packed ARGB, for example 4283215696. Omitted when the accessory has no color set. This is the one visual property in the whole format that travels, because it is not decoration: people name a band by its color, so importing "Green band" as a gray swatch would throw away something the file plainly said. (Superset colors, by contrast, stay behind - the recipient's palette assigns those.)
label string Free text shown beside the name, for example "rated 25 lbs". Carried word for word and never parsed at either end. Accessories never feed any calculation in Badger, so a number in here stays a piece of text.
notesstringFree text about the accessory.

Rack

The objects in racks: the dumbbells or kettlebells a gym physically owns. Badger keeps these as a list rather than a step size because a real rack is not a grid - it runs 5-10-15 up to 50 and then jumps, and a kettlebell ladder is not a multiple of anything useful. The list is what the plus and minus buttons walk when you are logging.

FieldTypeMeaning
equipmentstring, required"dumbbell" or "kettlebell". A value the reader does not know is skipped rather than guessed at.
unitstring, required"kg" or "lbs", for every weight in this rack.
weights array of objects, required Each one is a weight (number, required) and an optional color (packed ARGB integer). The color is there for competition kettlebells, which are identified by it - the same reasoning as an accessory's. Leave it out for an unpainted weight; nothing invents one.

One entry per equipment-and-unit pair, so a gym with kilogram kettlebells and pound dumbbells carries two. They are separate lists and nothing is converted between them: 45 in a pound rack is a 45 lb dumbbell, not a 99 lb one.

PlateInventory

The objects in plate_inventory: what the plate calculator has to work with.

FieldTypeMeaning
unitstring, required"kg" or "lbs", for every number in this entry.
bar_weightnumberThe bar the calculator starts from, in the unit above - so 45 in a pound entry is a 45 lb bar. Absent falls back to the ordinary bar for that unit.
plates array of objects, required Each one is a weight (number, required), a color (packed ARGB integer), a quantity (how many are available per side, defaults to 2) and enabled (defaults to true). A plate that exists but is switched off is a real state, so both flags are written out.

One entry per unit, and again nothing converts. A file only carries a plate list somebody actually set up - a gym running on Badger's built-in defaults writes no plate_inventory at all, because those defaults are already in the app opening the file.

type: "machines" is the smaller version: one key, machines, an array of Machine and no gym, bars, accessories, or inventory.

A worked example

One routine, one day scheduled Monday and Thursday, two exercises. The bench press has a warmup set and three working sets of 8-10; the plank is timed. This is the real example-routine.badger, printed with indentation added: files the app writes are minified, and whitespace makes no difference either way.

{
  "badger": "share/v1",
  "app_version": "1.1.1",
  "created_at": "2026-07-25T23:42:17.697472Z",
  "type": "routine",
  "payload": {
    "routine": {
      "name": "Example Routine",
      "notes": "A two-exercise day, kept small enough to read.",
      "weight_source": "last_performance",
      "history_scope": "any_workout"
    },
    "days": [
      {
        "name": "Day 1",
        "scheduled_days": [
          1,
          4
        ],
        "exercises": [
          {
            "exercise_name": "Flat Barbell Bench Press",
            "rest_override_seconds": 180,
            "exercise_type": "weight_reps",
            "predefined_sets": [
              {
                "is_warmup": true,
                "reps": 8
              },
              {
                "is_warmup": false,
                "reps": 8,
                "reps_max": 10
              },
              {
                "is_warmup": false,
                "reps": 8,
                "reps_max": 10
              },
              {
                "is_warmup": false,
                "reps": 8,
                "reps_max": 10
              }
            ]
          },
          {
            "exercise_name": "Plank",
            "exercise_type": "time_only",
            "predefined_sets": [
              {
                "is_warmup": false,
                "duration_seconds": 60
              },
              {
                "is_warmup": false,
                "duration_seconds": 60
              }
            ]
          }
        ]
      }
    ],
    "exercises": [
      {
        "name": "Flat Barbell Bench Press",
        "type": "weight_reps",
        "category_name": "Chest"
      },
      {
        "name": "Plank",
        "type": "time_only",
        "category_name": "Core"
      }
    ],
    "machines": []
  }
}

What import does

Reading the file is the easy half. These are the rules that decide what it turns into.

  • Nothing is ever overwritten. Import only adds. An existing exercise, machine, gym, or routine is never edited by a file you open.
  • Exercises match on name and type, ignoring case and surrounding spaces. Same name, different type, means a different exercise, so a routine can never wire itself to the wrong one. No match, and the exercise is created, in the named category, which is itself created if it is new.
  • Machines, attachments, bars, accessories, gyms, and categories match on name alone, again ignoring case and spaces.
  • A routine name collision does not merge. Importing a routine whose name you already use gives you "Name (imported)", then "Name (imported) 2", and so on. You keep both and decide.
  • It is one transaction. A file that fails partway through leaves your database exactly as it was.
  • Order is array order. Days, slots, and sets import in the order they appear in their arrays - JSON arrays are ordered, and that order is the order. (Older files carrying the retired sort_order field still read; the field is ignored.)
  • A warmup never carries a rep range or an open target. If a file puts reps_max or is_amrap on a warmup set, import drops it rather than storing a plan the app cannot show you. A working set carrying both has its range dropped: open wins.
  • Files over 10 MB are refused without being read. A real export is a few kilobytes, so this only ever catches something that has gone wrong.

What a file does not carry

Worth knowing before you assume a share is a clone. A routine file is the plan, and only the parts of the plan that travel usefully between two different lifters in two different gyms.

  • No history. No workouts, no logged sets, no PRs. Those are yours and they stay on your phone. To move your own history to a new phone, use a backup instead: see moving to a new phone.
  • No notes below the routine level. Routine notes travel; per-day and per-exercise notes do not. A gym file is the exception that proves the rule: notes on a machine, attachment, bar or accessory travel, because they describe the equipment. The setup cues you save against one exercise on one machine never do - those describe you.
  • No default attachment, default bar, or default grip on a slot - those stay personal setup. Superset group color also stays behind; the recipient's palette assigns it. (An accessory's color does travel, because that one is the thing's name rather than decoration.)
  • No per-exercise detail beyond name, type, and category. An exercise that gets created on import starts blank: no instructions, no muscles, no rest default, no increment. Anything you already had keeps everything it already has.
  • No photos of machines, attachments, or bars. A .badger file is a small text document, and your photo of a leg press is yours.
  • Nothing about you. No account, no device identifier, no settings, no training notes.

The versioning promise

The version string exists so that files stay readable. Two commitments go with it. Unknown fields are ignored, so a file written by a newer Badger still opens in an older one, minus whatever that older version has no concept of. And if the shape ever has to change in a way that breaks, badger gets a new value and share/v1 keeps being read: a file you exported today should still import years from now.

That is the same commitment as the rest of the promise. Data you can get out is only worth something if it still opens later.

Verified

Last checked against Badger 1.1.1, schema share/v1. The example above and the files in the program gallery were exported by that build and imported back into it before publishing.