spatariurares3k downloadsTrack workouts in your notes: log sets, follow a guided session, and see progress with tables, charts, timers and a dashboard with PRs, trends and a muscle heat map. Data stays in one CSV file.
English · العربية · Čeština · Dansk · Deutsch · Español · Français · हिन्दी · Bahasa Indonesia · Italiano · 日本語 · 한국어 · Nederlands · Norsk bokmål · Polski · Português (Brasil) · Português (Portugal) · Română · Русский · Shqip · Türkçe · Українська · 简体中文 · 繁體中文
The all-in-one workout tracker for Obsidian. Lifting, running, cycling, yoga or exercises with your own fields: log from your phone, follow a guided session, and see your progress in charts, a dashboard and a muscle heat map. One CSV, 24 languages.
Built for your phone: log each set, run or pose as you train, then watch your progress in tables, charts and a dashboard inside the same notes where you plan your training. Your sets go into one CSV file in your vault. No account, no cloud.
Charts show your trend, the muscle heat map shows what you trained, and each set keeps its own kg/lb unit. You can query your sets from Dataview.

The first time you install the plugin, it adds example notes (sample exercises, two workouts with six weeks of logs, a dashboard and a feature showcase), points the plugin at them and opens the Getting started guide: one short page per feature, with working examples you can try. You don't need to open settings. Poke around before setting up your own.
Want the examples or the guide again? Select Open getting started guide under Settings → Workout Planner → Getting started (it adds the example notes that are missing first), or run Create example notes or Open getting started guide from the command palette (Ctrl/Cmd + P). Creating the examples never overwrites anything: running it again only adds the example files that are missing, and files you changed stay as they are. The guide offers to create the examples if they don't exist yet. An empty table, chart or dashboard also has an Or explore with example data link. If you already have a workout log, the examples get their own log file and yours stays untouched.
To add a table to any note, for example Push Day:
```workout-log
exercise: Bench Press
```
Tap + under the table, enter reps and weight, and save. The table updates on its own. You don't have to write blocks by hand either: the Insert workout chart / table / sets / timer / dashboard / duration and Insert guided session commands open a form (or write the block) for you.
Open the log form with the + under a table (the exercise is filled in for you), the dumbbell icon in the ribbon, or the Create workout log command. The form starts from your last set, so most of the time you tap +/− once and save. Done before your rest is over.
The form asks for the fields that fit the exercise: reps and weight for a squat, seconds for a plank, distance and time for a run (see Exercise types). Each set can also carry a note and a protocol, shown as a badge in the table.
The weight field has a kg/lb picker, so you can log 225 lb even if your setting is kg. The table shows that set as "225 lb"; charts, stats and volume convert every set to your setting.
When you log a set from a note, the note's name is saved as the set's workout, so a table or chart with workout: Push Day finds it later.
A workout-log block shows the sets of an exercise or workout, newest first. Sets from the same day are grouped under a row with that day's totals, and each row has an edit button; delete a set from the edit form. On a phone each set becomes a compact row with no sideways scrolling.
exercise: Bench Press
dateRange: 14
targetWeight: 100
targetReps: 8
With targetWeight and targetReps, a progressive-overload target appears above the table.
| Parameter | Default | What it does |
|---|---|---|
exercise |
— | Only this exercise |
workout |
— | Only this workout (note name) |
exactMatch |
false |
Match the exercise name exactly |
dateRange |
all | Today and the previous N days |
limit |
50 |
Maximum rows; with sets, sets × 2 (two workouts) |
sortBy |
date |
date, exercise, weight, reps or volume |
sortOrder |
desc |
asc or desc |
columns |
all | Columns to show, e.g. ["date", "reps", "weight"] |
showProtocol |
true |
Show the protocol badge column |
showAddButton |
true |
Show the + button |
targetWeight, targetReps |
— | Show a progressive-overload target above the table |
sets |
— | Sets planned, counted by a workout-session block |
Exercise names match loosely by default ("squat" finds "Squat (high bar)"); add exactMatch: true for only that exact name.
A workout-sets block is made for logging at the gym: today's sets of one exercise as rows to fill in. Each row shows what you did in the same set last time, so you type only what changed (an empty field logs that value) and tick the row. The set goes into the log, the row turns into a logged set, and the rest timer at the top starts on its own. A logged set's chevron changes its protocol and notes; ticking it again takes it back, with Undo for a few seconds.
exercise: Squat
sets: 4
rest: 120
Insert workout sets from the command palette writes one for you, and the ⋮ menu edits it or its rest timer.
| Parameter | Default | What it does |
|---|---|---|
exercise |
— | The exercise (exact name) |
workout |
the note name | The workout the sets are logged under |
sets |
as last time | Rows to fill in (at least one); Add set adds more |
targetWeight, targetReps |
— | Hints when the exercise was never done, and a progressive-overload target |
rest |
— | Seconds of rest after each set; 0 for no timer. Unset: the workout-timer of the same section |
timer.* |
— | The rest timer's other options: timer.type, timer.preset, timer.rest, timer.rounds, timer.showControls, timer.sound |
A workout-chart block draws one measure over time: weight, volume, reps, estimated 1RM, duration, distance, pace or heart rate. The header says whether the trend is Increasing, Stable or Decreasing. Stable means the trend line moves less than 5% of the average over the whole period, so a slow, steady gain still counts as increasing. For pace, lower is better, and a falling line shows as improving. On a phone the chart becomes a table of the same values.
Use type: e1rm for the best daily estimated one-rep max (Epley: weight × (1 + reps / 30)). Only positive weights with integer reps from 1 to 12 count; warm-up and drop sets are excluded. Days without eligible sets are omitted. Estimates follow your kg/lb setting. The default strength chart remains volume.
exercise: Bench Press
type: e1rm
dateRange: 180
The API adds estimatedOneRepMax to getExerciseStats: the best estimate in the configured weight unit, or null when no eligible set exists. Existing PR fields and CSV columns are unchanged.

exercise: Squat
type: volume
dateRange: 90
showTrendLine: true
| Parameter | Default | What it does |
|---|---|---|
exercise |
— | Only this exercise |
workout |
— | Only this workout |
type |
volume |
volume, weight, reps, e1rm, duration, distance, pace or heartRate |
chartType |
exercise |
How each day's point is built (below) |
dateRange |
30 |
Days to include |
limit |
— | Only the latest N points |
showTrendLine |
false |
Draw a trend line |
showTrend |
true |
Show the trend header above the chart |
showStats |
true |
Show average, best and lowest under the chart |
title |
Trend <type> |
Chart title |
height |
4:3 | 300 (pixels) or any CSS length such as 50vh |
exactMatch |
false |
Match the exercise name exactly |
chartType |
Sets used | Each day's point |
|---|---|---|
exercise |
sets of exercise |
average |
workout |
sets of workout |
total |
combined |
sets of exercise within workout |
total |
all |
every set (filters ignored) | total |
A workout-dashboard block opens with four numbers: total workouts, weekly streak, total volume and personal records. Below them you get quick stats for the week, month and year, a volume trend, your recent workouts, protocol stats, planned vs. actual workout length and a training calendar: a year of training days, shaded by volume, with your current and best streak. A week counts towards the streak once it has the training days of your weekly goal (Settings → Weekly goal, 1 by default). On a phone, quick actions come first and the summary is a 2 × 2 grid.
title: Last three months
dateRange: 90
widgets: [summary, heatMap, volume, recentWorkouts]
To choose the widgets and their order without writing YAML, insert the dashboard with the Insert workout dashboard command, or open the ⋮ menu at the top right of an inserted dashboard and pick Edit dashboard.
Set Layout to Grid in that window to place each widget yourself, like items in an inventory: drag a widget to move it, drag its corner to make it wider or taller, and drop it under the grid to take it off. The keyboard works too: arrows move, Shift and arrows resize, Delete takes a widget off. The grid has as many columns as fit the width of your note; in a narrower pane the widgets are packed again in reading order, and on a phone they are stacked. Your layout is kept: when the note fits only one column (a phone), the window asks how many columns to edit, starting from the saved ones. The block keeps one line per widget (column, row, width, height):
layout: grid
columns: 3
grid.summary: 1 1 3 1
grid.heatMap: 1 2 2 2
grid.recentWorkouts: 3 2 1 1
The heat map shows which muscles you trained over the last week, month or year, front and back, by Volume, Sets or Reps. It's the quickest way to notice you've skipped legs for three weeks. Under it, a short training analysis warns you about imbalances, like a front that works much more than the back. It reads the muscle tags of your exercise pages. Bodyweight and timed exercises have no weight, so they light up only under Sets and Reps.
| Parameter | Default | What it does |
|---|---|---|
title |
— | Heading above the dashboard |
dateRange |
all | Only sets from the last N days |
widgets |
all | The widgets to show, in this order: summary, quickStats, heatMap, volume, recentWorkouts, protocolDistribution, protocolEffectiveness, duration, quickActions, fileErrors, muscleTags, trainingCalendar |
layout |
list |
grid places each widget in its cells (see above) |
columns |
— | Grid: the columns the grid was made for |
grid.<widget> |
— | Grid: column, row, width and height of that widget |
recentWorkoutsLimit |
5 |
Workouts in the recent list |
volumeTrendDays |
30 |
Days in the volume trend chart |
Tags can be a list, a single word (tags: chest) or a comma-separated line (tags: chest, triceps).
Broad groups: chest, back, shoulders, biceps, triceps, forearms, traps, quads, hamstrings, glutes, calves, abs, core
Specific muscles have their own area of the body and count toward their group. A broad tag spreads over the whole area: chest lights all three parts of the chest a little, upper_chest lights only the top one fully.
| Group | Specific muscles |
|---|---|
chest |
upper_chest, mid_chest, lower_chest |
shoulders |
front_delts, side_delts, rear_delts |
back |
lats, rhomboids, lower_back (or ql) |
core |
obliques, serratus |
Tags in your own language, like petto or schiena, work too. The plugin maps them to the groups above using muscle-tags.csv, next to your log. Edit it with Manage muscle tags (add, rename, search, spot duplicates) or by hand:
tag,muscleGroup,language
petto,chest,it
dorsali,lats,it
A tag that is already a group name (upper_chest, Upper chest, upper-chest) always works, even if muscle-tags.csv doesn't list it.
A workout-timer block is a countdown, interval or stopwatch timer. Save your usual rest times as presets. Link a timer to an exercise or workout and it restarts every time you log a set, so you don't have to touch it between sets. With autoStart: true it starts as soon as the note opens.
It beeps on iOS and Android too. Phones only allow sound after you've tapped the page, so a timer started by autoStart stays silent until you tap Start once.
Under Settings → Workout Planner → Timer device cues, Keep screen awake while a timer runs is on by default. It requests screen protection while at least one timer runs in the foreground, where the device allows it. Vibrate when a timer completes is off by default and adds a brief pulse at zero, independently of sound, on supported devices such as Android. iOS keeps the existing audio cue; web vibration is unavailable there. These features use browser APIs on a best-effort basis and have not yet been verified on physical Android/iOS devices. They do not provide background alarms. A timer with an id keeps running when you change notes; pause it or let it finish to release screen protection.
type: countdown
duration: 90
exercise: Bench Press
An interval timer can alternate work and rest. The display shows Work or Rest, and each phase starts with its own sound. Each round is its work followed by its rest, the last one included, so this Tabata runs 4:00:
type: interval
work: 20
rest: 10
rounds: 8
preset: Tabata (20 s work, 10 s rest, 8 rounds) and preset: EMOM (a round every minute for 10 minutes) are built in. A saved preset with the same name replaces them.
| Parameter | Default | What it does |
|---|---|---|
type |
countdown |
countdown, interval or stopwatch |
duration |
30 |
Seconds (countdown; for interval the work of each round) |
work |
— | Interval: seconds of work per round (replaces duration) |
rest |
0 |
Interval: seconds of rest after each round's work |
rounds |
1 |
Rounds (interval) |
sound |
true |
Beep when time is up |
autoStart |
false |
Start as soon as the note opens |
showControls |
true |
Show start, pause and reset |
preset |
— | Start from a preset (saved, Tabata or EMOM); parameters override it |
exercise, workout |
— | Restart the timer when you log a set for this exercise or workout |
superset |
false |
Canvas export only: draw this exercise and the next one as a superset |
A workout-session block at the top of a workout note (command Insert guided session) walks you through it one exercise at a time: which set you're on, the target, what you did last time, a Log set button and the rest timer, which starts when you log a set.

```workout-session
```
## Squat
```workout-sets
exercise: Squat
sets: 4
targetReps: 8
targetWeight: 100
rest: 180
```
The plan is the note itself: each workout-log or workout-sets block with an exercise is a step, in the order they appear, with its sets, targetReps and targetWeight. The rest comes from the workout-sets block's own rest, else from the workout-timer in the same ## section (the default preset without one). The session and the sets card share one countdown. The session counts the sets you logged today in this workout, so it picks up where you left off when you reopen the note. An exercise without sets stays current until you log a set of a later one; Previous and Next move freely.
A workout-duration block estimates how long a workout note takes: the rest from its timers plus the time for each set, next to how long your last session really took.
workout: Workouts/Push Day.md
workout can be a path in the vault, a path relative to this note, or just the note's name; leave it out to estimate the current note. Seconds per rep and per set come from Settings.
An exercise page is a note in your exercise folder. Its frontmatter says what the log form should ask for and which muscles the exercise trains. Create exercise page makes one for you.
---
exercise_type: strength
tags:
- chest
- triceps
---
exercise_type |
The log form asks for | Also accepted |
|---|---|---|
strength (default) |
reps, weight | bodyweight, weights |
timed |
duration | duration, time, timer, interval, hold, isometric |
distance |
distance, duration (optional) | running, run |
cardio |
duration, distance and heart rate (optional) | — |
custom |
only the fields you list under parameters |
— |
Yoga works the same way: each pose is a timed exercise, so the form asks only how long you held it. The example notes include a morning yoga flow with a guided session.

For a sport that logs something else (a climbing grade, attempts, whether you sent it), make your own type in Settings → Exercise types: give it a name and pick its fields, built-in ones like reps and weight or new ones (number, text or yes/no). It then shows up in the type list when you create an exercise, which writes exercise_type: <your type> in the page. The built-in types can't be changed, but Duplicate starts a type from one. Deleting a type only stops offering it: exercises that use it keep their fields and logged sets.
type: works the same as exercise_type:. You can also add fields to one exercise under parameters, for example rounds for jump rope:
---
exercise_type: timed
parameters:
- key: rounds
label: Rounds
type: number
required: false
---
If you change an exercise's type after logging it, Convert exercise moves the existing sets over and lets you choose which old field goes where.
Every set is one row in workout_logs.csv, so you can open it in a spreadsheet, back it up, or sync it like any other file.
date,exercise,reps,weight,volume,origine,workout,timestamp,notes,protocol
2025-01-17T10:30:00.000Z,Bench Press,8,100,800,[[Push Day]],Push Day,1737138600000,,standard
| Column | Meaning |
|---|---|
date |
When the set was logged (ISO 8601) |
exercise, workout |
Exercise and workout name |
reps, weight, volume |
Volume is reps × weight |
origine |
Link to the note it was logged from |
timestamp |
Unique ID of the row (milliseconds) |
notes, protocol |
Free text and training technique |
Exercise types with their own fields (duration, distance, heartRate, or your parameters) add columns at the end. The last column, weightUnit, is kg or lb: the unit of that row's weight and volume. An empty value means the unit in the settings.
If you edit the file by hand, keep the header row, and wrap any value that contains a comma, quote or line break in double quotes. Notes starting with =, +, - or @ are stored with a leading ' so spreadsheets don't run them as formulas; the plugin hides it again.
Everything stays in your vault. The plugin writes to the clipboard only when you click a copy badge (a muscle tag in the dashboard), and never reads it.
The plugin exposes WorkoutPlannerAPI (also window.WorkoutPlannerAPI) once it has loaded.
const s = await WorkoutPlannerAPI.getExerciseStats("Bench Press");
dv.paragraph(`Best: ${s.prWeight} × ${s.prReps} on ${s.prDate}`);
| Method | Returns |
|---|---|
getWorkoutLogs(filter?) |
Sets, each with date, exercise, reps, weight, volume, workout, notes, timestamp, protocol. weight and volume are in the settings unit; enteredWeight and enteredUnit are what was logged |
getExerciseStats(exercise) |
estimatedOneRepMax, totalVolume, maxWeight, prWeight, prReps, prDate, totalSets, averageWeight, averageReps, lastWorkoutDate, trend |
getExercises(filter?) |
Exercise names, sorted: the file names of your exercise pages (subfolders included), or the names in your log when you have no exercise pages. { tag: "chest" } keeps the exercises whose page has that tag, so it is empty without exercise pages. |
getWorkoutLogs accepts exercise (partial, case-insensitive), workout, dateRange: { start, end }, protocol and exactMatch.
Recent squats, in Dataview:
const logs = await WorkoutPlannerAPI.getWorkoutLogs({
exercise: "Squat",
dateRange: { start: moment().subtract(30, "days").format("YYYY-MM-DD") },
});
dv.table(
["Date", "Reps", "Weight"],
logs.map((l) => [l.date.slice(0, 10), l.reps, l.weight]),
);
In a Templater template, to start a note with your latest numbers:
## Squat
<%\*
const s = await WorkoutPlannerAPI.getExerciseStats("Squat");
tR += s.totalSets
? `Best ${s.prWeight} × ${s.prReps} (${s.prDate}), last session ${s.lastWorkoutDate}`
: "No squats logged yet";
%>
Set the CSV and exercise folders, the default weight unit, the weight step of the +/− buttons, timer presets, your own protocols (name, short label and color), and the seconds per rep and per set used by the duration estimate. Run all under Advanced runs every maintenance task after an upgrade.
Changing the weight unit doesn't relabel old sets: each set keeps the unit it was logged in.
Open the command palette (Ctrl/Cmd + P) and type the name.
| Command | What it does |
|---|---|
| Create workout log | Log a set |
| Create example notes | Add sample exercises, workouts, logs and a dashboard |
| Open getting started guide, Start the tour | Open the guide in the examples |
| Show what's new | Show what changed in each version |
| Insert workout chart / table / sets / timer / dashboard / duration | Insert a block through a form |
| Insert guided session | Insert a workout-session block at the cursor |
| Create exercise page | New exercise page with type and tags |
| Add exercise block | Add a section for an exercise with its sets block, with name autocomplete |
| Convert exercise | Move an exercise's sets to another exercise type |
| Manage muscle tags | Edit muscle-tags.csv |
| Generate tag reference | Create a note listing every muscle tag |
| Audit exercise names | Find the same exercise logged under different spellings |
| Export workout to canvas | Lay out the current workout on an Obsidian canvas |
| Create CSV log file | Create an empty log |
| Import from Strong or Hevy | Add the sets of a Strong or Hevy CSV export to the log |
| Migrate exercise types, Add missing IDs to code blocks | Maintenance after upgrading from older versions |
Conflict with Workout Block (workout-block). Both plugins register workout-log code blocks and use workout_logs.csv as the default log file name. Disable one of the two plugins before using the other.
A table or chart is empty. Check the exercise name and the dateRange (charts default to the last 30 days). With exactMatch: true the name must match exactly.
An exercise is missing from the heat map. Its page needs muscle tags and must be in the exercise folder. Bodyweight and timed exercises show up only under Sets or Reps.
The log form asks for reps and weight for a plank. Set exercise_type: timed in the exercise page.
The timer makes no sound on my phone. Tap Start once; phones block sound until you touch the page.
main.js, manifest.json and styles.css from the latest releaseyour-vault/.obsidian/plugins/workout-planner/Workout Planner is free and open source. If it helps your training, you can buy me a coffee. Found a bug or missing something? Open an issue.
Every language except English is machine-translated, so some phrases may sound off. If yours reads strangely, corrections are welcome.
Charts use Chart.js (MIT). Workout Planner is released under the MIT License.