Exercise Logging in Obsidian
I use Obsidian to track all my workouts as a simple entry in my "Daily Note". These entries are then queryable with Dataview to create historical views of workouts with various filters applied. My tracking is a simple text entry in the file:
exercise:: squats - 190 - 5,5,5,3
With the format being roughly: exercise name, sets, reps. I often put additional notes in there, like what settings I use on a machine. Every time I start a workout I have a rough sense in my head of what exercises I plan to do, but I don't really remember what weights I should be using. Thus starts the processing of scrolling through my daily notes to find the last time I did this exercise, and using that as a baseline. I maintain a set of "weekly notes" that summarize this, but I haven't been good about generating these in the past few weeks, and it's still not ideal to have to scroll back and forth between the exercise entry and the weekly note.
So what do we want? Ideally, I want to enter the current exercise I'm doing. I should then see the past few times I've done this exercise, along with weight and rep ranges. I'll enter the current weight and reps I'm doing as I perform the exercise. When I'm done, I want to "click a button" and have that logged to my daily note in the same format as before.
To implement this search functionality, I turned to Dataview. I created a new note at notes/forever/exercise
that had 3 properties: c_exercise
, c_weight
, c_reps
- where the c_
prefix stands for current. I then wrote up a Dataview query (after a lot of trial and error) to search in a given window for matching exercises:
```dataviewjs
const windowInDays = 30;
const windowInMs = 1000 * 60 * 60 * 24 * windowInDays;
const today = new Date();
function matches(entry, query) {
if (entry.includes(query)) return true;
// check if all the individual words in the query are in the exercise
const queryWords = query.split(' ');
return queryWords.every(word => entry.includes(word))
}
const allExerciseEntries = Array.from(dv.pages('"daily"').filter(p => {
const pDate = new Date(`${p.file.name}`);
return (today - pDate) < windowInMs;
}).flatMap(p => {
let exercise = []
if (Array.isArray(p.exercise)) {
exercise = p.exercise
} else if (typeof p.exercise === 'string') {
exercise = [p.exercise]
}
return Array.from(exercise).map(e => [p.file.name, e])
}))
const { c_exercise } = dv.pages(`"${this.currentFilePath}"`)[0];
let rows = []
if (c_exercise) {
for (const entry of allExerciseEntries) {
if (matches(entry[1], c_exercise)) {
rows.push(entry)
}
}
}
rows = rows.sort((a, b) => new Date(b[0]) - new Date(a[0]))
dv.table(["Date", "Exercise"], rows)
```
Some notes about the above code snippet:
- The
exercise
property is sometimes a string if I've only done one thing that day (e.g. "Biking"), so that has to be handled specially. - Dataview queries have a slightly quirky format where you have to pass the quotes into the query string, so not
dv.query("pageName")
, butdv.Query('"PageName"')
. - The search function is really simple - we look for exact matches of the query string in the exercises OR for exercises that have every single word in the query string (but in any order). This was good enough for my use case where I would log the same exercise as
pull-up
orpull ups
orpull-ups
. The nomenclature I gave exercises changed slightly week over week, but never enough that this algorithm didn't work.
This worked but the table was really slow to update when I changed the query string. After some digging, I realized that Dataview has a default update window of 2.5 seconds after a file is changed. Dropping this down to 100ms globally made things work really smoothly!
To actually log the exercise entry I used a QuickAdd capture with an exercise template. The exercise template was really simple:
<%*
const dv = this.app.plugins.plugins["dataview"].api;
const filePath = "notes/forever/exercise";
const { c_exercise, c_weight, c_reps } = dv.pages(`"${filePath}"`)[0];
tR += `${c_exercise} - ${c_weight || 'n/a'} - ${c_reps || 'n/a'}`
%>
You can then configure QuickAdd to add this entry to your existing note with a capture format of exercise:: {{TEMPLATE:notes/forever/exercise.md}}
.
All this worked really well on my laptop, but I ran into syncing hell when trying to get it on my iPhone. The first problem was that the exercise capture was being overwritten by an old capture definition I had on my phone - I fixed this by just entering the template definition on my phone. The other problem that I still haven't fixed is the template file sometimes gets overwritten with the template data - a spooky bug for a spooky time.
A few things I want to do in the future:
- Clear out the existing frontmatter automatically - I tried using Obsidian's
processFrontMatter
API to do this but gave up after a few failed attempts. - Have better fuzzy searching
- Define workout templates up front so they aren't in my head
- Have analytics on how often I'm working out different parts/ muscle groups, and my progression on certain lifts or machines. I'm imagining some LLM integrated automatic tagging system so I don't have to manually enter tags for each exercise.