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.
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.
| Field | Type | Meaning |
|---|---|---|
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:
| Key | Type | Contents |
|---|---|---|
routine | Routine | The routine itself: name, notes, and its settings. |
days | array of Day | The days, in order. Each day carries its own exercises array: that day's slots, in order, with their planned sets. |
exercises | array of Exercise | The definition of every exercise any day references: its name, type, and category. Day slots point into this list by exercise_name. |
machines | array of Machine | Machines 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | The 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | Day 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. |
exercises | array of DayExercise | The day's exercise slots. |
DayExercise
The objects in a Day's exercises array: one slot in the plan.
| Field | Type | Meaning |
|---|---|---|
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.
| Field | Type | Meaning |
|---|---|---|
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_seconds | integer | Planned time, for timed exercises. |
distance | number | Planned 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.
| Field | Type | Meaning |
|---|---|---|
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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | Machine name. |
notes | string | Free 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_attachments | boolean, defaults to false | Whether the machine takes attachments (bars, handles, ropes). |
uses_grip_types | boolean, defaults to false | Whether 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.
| Field | Type | Meaning |
|---|---|---|
gym | Gym | A Gym is just { "name": "..." }. |
machines | array of Machine | The same Machine objects a routine payload carries. |
attachments | array of Attachment | An Attachment is a name and optional notes. |
bars | array of Bar | Named bars. Omitted entirely when the gym has none, so a bar-free file looks exactly like one written before bars existed. |
accessories | array of Accessory | Bands, chains, and straps. Omitted entirely when the gym has none. |
gym_machines | array of strings | Names of the machines that belong to this gym. |
gym_attachments | array of strings | Names of the attachments that belong to this gym. |
gym_bars | array of strings | Names of the bars that belong to this gym. |
gym_accessories | array of strings | Names of the accessories that belong to this gym. |
racks | array of Rack | The gym's dumbbell and kettlebell racks. Omitted entirely when the gym has none set up. |
plate_inventory | array of PlateInventory | The 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | Bar name, for example "Olympic bar" or "Trap bar". |
notes | string | Free 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.
| Field | Type | Meaning |
|---|---|---|
name | string, required | Accessory 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.
|
notes | string | Free 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.
| Field | Type | Meaning |
|---|---|---|
equipment | string, required | "dumbbell" or "kettlebell". A value the reader does not know is skipped rather than guessed at. |
unit | string, 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.
| Field | Type | Meaning |
|---|---|---|
unit | string, required | "kg" or "lbs", for every number in this entry. |
bar_weight | number | The 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_orderfield still read; the field is ignored.) - A warmup never carries a rep range or an open target. If a file puts
reps_maxoris_amrapon 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
.badgerfile 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.
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.