Using fswatch to dynamically update Obsidian documents

Although I’m a relative newcomer to Obsidian, I like what I see, especially the templating and data access functionality - both that provided natively and through the Templater and Dataview plugins.

One missing piece is the ability to dynamically update the YAML-formatted metadata in the frontmatter of Obsidian’s Markdown documents. Several threads on both the official support forums and on r/ObsidianMD have addressed this; and there seems to be no real solution.1 None proposed solution - mainly view Dataview inline queries or Templater dynamic commands - seems to work consistently.

The solution proposed here is a proof-of-concept for an entirely different way of addressing the problem. But it requires getting dirty with more command line programming than many may want to contend with. If you do, the basic idea is to watch the vault directories for changes and update the YAML directly outside of Obsidian.

Use case

I have a YAML field mdate: which is the date last modified. Whenever the file is touched, I would like the mdate: field updated. Here’s a sample of my frontmatter:

---
uid:	20210517060102
cdate:	2021-05-17 06:01 
mdate:  2022-11-18 20:06
type:	zettel
---

Straightforward, right?

Solution

As I was unable to implement a solution inside Obsidian, I turned to fswatch which is a cross-platform filesystem watcher. When certain events occur in a watched directory, it reports those events in user space.

#!/bin/bash

FP="path/to/my/vault"

function update_mdate() {
   FILE="$1"
   if uname | grep -q "Darwin"; then
      MODDATE=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$FILE")
   else
      MODDATE=$(stat -f "-c %y" "$FILE" \
         | xargs \
         | awk '{split($0,a,":"); printf "%s:%s\n", a[1], a[2]}' )
   fi
   sed -i '' -E "s/(modification date:).*/\1  $MODDATE/g" "$FILE"
   sed -i '' -E "s/(mdate:).*/\1  $MODDATE/g" "$FILE"
}

/usr/local/bin/fswatch -0 --format="%p|%f" $FP | while read -d "" event; do
   [[ $event =~ ".DS_Store" ]] && continue
   [[ $event =~ "IsDir" ]] && continue 
   [[ ! $event =~ "Updated" ]] && continue
   
   # ignore anything that's not a Markdown file
   [[ ! $event =~ ".md" ]] && continue 
   
   # ignore file removal events
   [[ $event =~ "Removed" ]] && continue
   # ignore swap file bs
   [[ $event =~ ".swp" ]] && continue
   
   # Ignore what may be swap files that Obsidian uses
   UPDATED_FILE=$(echo "$event" | cut -d "|" -f1)
   [[ ! $event =~ ".!" ]] && update_mdate "$UPDATED_FILE"
done

I’ll try to explain the highlights of the above code. The main loop is around the fswatch invocation. I won’t go into depth with the --format parameter; but essentially we are looking for the file that has been altered and the event list.

Most of the remaining logic of this loop is to filter out unwanted events, directories, and files. For example:

[[ ! $event =~ "Updated" ]] && continue

ensures that only Updated events will be processed. The rest of these filters are fairly self-explanatory.

One additional feature of these event and file filters that does need to be mentioned is that it appears Obsidian writes some kind of temporary swap file before committing changes to the main file. These files have a naming convention which is just the main filename prefixed with “.!”, so the following logic only processes files that do not have this pattern:

# Ignore what may be swap files that Obsidian uses
UPDATED_FILE=$(echo "$event" | cut -d "|" -f1)
[[ ! $event =~ ".!" ]] && update_mdate "$UPDATED_FILE"

Updating the mdate

The logic for updating the modified date parameter is embedded in the Bash function update_mdate.

function update_mdate() {
   FILE="$1"
   if uname | grep -q "Darwin"; then
      MODDATE=$(stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$FILE")
   else
      MODDATE=$(stat -f "-c %y" "$FILE" \
         | xargs \
         | awk '{split($0,a,":"); printf "%s:%s\n", a[1], a[2]}' )
   fi
   sed -i '' -E "s/(modification date:).*/\1  $MODDATE/g" "$FILE"
   sed -i '' -E "s/(mdate:).*/\1  $MODDATE/g" "$FILE"
}

Most of the complexity in the date-updating function is in handling stat differently depending on the platform. macOS uses the BSD version of stat but Linux uses the coreutils version. After parsing the last modified date from stat we use sed to “splice it into” our document. Some of my documents use mdate: and others have used modification date: so we handle both. I haven’t had the chance to do thorough testing of the Linux piece, but I believe it should work.

Then we just have to worry about how to keep the script running. On my macOS machine, I use LaunchControl to set it up as a User Agent. If you’re comfortable with launchd then you can set it up directly with the help of a GUI application.


  1. For example, this thread on the official Obsidian forums which discusses the issue with using dynamic queries in the YAML. One solution offered was to embedded the dynamic command in apostrophes: ‘<%+ tp.file.last_modified_date() %>’ This did not work in my case, nor did it work in the case of at least one other respondent. As of right now, I don’t think there’s a good solution, apart from the approach suggested in this article, if you want the frontmatter YAML to update dynamically. ↩︎