Automating the Repair of Broken Daily Note Links in Obsidian with Python
In the world of personal knowledge management and digital note-taking, Obsidian is one of the premier tools. Its ability to create and navigate between linked notes makes it popular among researchers, writers, and knowledge workers in general.
One my favourite features of Obsidian is its ability to create links between notes, similar to a wiki but for personal use. Fans say that this enables the creation of a “second brain” or a “digital garden” where your notes can interweave and grow organically over time. This second brain is a topic for another time, but my other favourite feature of Obsidian, which we will focus on today, is the Daily Note — a fresh note created each day for capturing thoughts, tasks, and snippets of information.
Each daily note can be made to link to the note for the previous and following days, allowing one to effortlessly navigate through the chronicle of my thoughts. That, at least, is the theory…
In practice, however, this flow breaks down when daily notes are not created for certain days; weekends, off-days, etc. This results in broken links, leading to non-existent daily notes — foiling my attempts to slide chronologically through them. I even found myself creating empty notes just to bridge these gaps. What a waste of time!
To remedy this, I needed to find a way to retrospectively fix these broken links and replace them with valid ones, jumping gaps where daily notes were absent.
Understanding the Problem
The heart of the issue is Obsidian’s automatic linking of daily notes to the previous and next days. Each note is a distinct text file, formatted in markdown, stored on a folder in your Obsidian ‘vault’. The names of the files reflect the date of that note. The links are generated when the note is first created thanks to an a plugin called Templater, that allows users to include dynamically generated information in the templates.
This creates a file with a name based on the today’s date and generates links to yesterday’s and tomorrow’s notes.
The code below is the part of the template where this happens. The first line names the new markdown file (E.g. 2003–06–16.md
). A piece of italic text is inserted, Daily Note. Then on the third line, links to the previous and next days (E.g. <<Thu 15 June 2023 | Sat 17 June 2023 >>
) are created.
# <% tp.date.now("ddd DD MMMM YYYY", 0, tp.file.title, "YYYY-MM-DD") %>
*Daily Note*
<< [[Daily/<% tp.date.now("YYYY-MM-DD", -1, tp.file.title, "YYYY-MM-DD") %>|<% tp.date.now("ddd DD MMMM YYYY", -1, tp.file.title, "YYYY-MM-DD") %>]] | [[Daily/<% tp.date.now("YYYY-MM-DD", 1, tp.file.title, "YYYY-MM-DD") %>|<% tp.date.now("ddd DD MMMM YYYY", 1, tp.file.title, "YYYY-MM-DD") %>]] >>
What I wanted was all the links in my notes to point to the actual previous and next notes, bypassing the days where no notes were taken. There is a possibility of a dynamic workaround (by Moonbase59) for this problem, using the dataview plugin in Obsidian. It is an neat trick for new notes, but it couldn’t retrospectively fix the links in all the notes I had previously generated. There were many of these!
Approaching the Solution
First, I got rid of those gap-filling notes. This was easy, because empty notes with no content all had the same minimum file size and I was able to order the notes by size in finder, and get rid of them in one go. I was left with a broken string of pearls (of wisdom?) that needed to be strung back together, using the date information embedded in the note filenames.
As my most recently acquired programming language, Python was a tempting option. Python is concise and has an extensive suite of file and date handling libraries, that made it an ideal choice for tackling this problem. The idea was to iterate through all the markdown files, extract the dates from the filenames, and identify the actual previous and next files chronologically. Then, within each file, replace the link line with a new string reflecting the correct previous and next files.
The Python Script
The Python script I created began by gathering the names all markdown files in the daily notes directory and sorting them in alphabetical order order, based on the text of the filename, and storing them in a list. Conveniently, this is the same as chronological order for filenames with a year-month-date name format.
files = [f for f in os.listdir(dir_path) if f.endswith('.md')]
files.sort()
For each file, I calculated the dates of the next and previous files by taking the date from the filename and adding or subtracting a day, as if notes for the previous and next day all existed.
prev_date = date_obj - timedelta(days=1)
next_date = date_obj + timedelta(days=1)
A while loop was implemented to adjust those dates incrementally. In each iteration of that loop, checks were performed to see if markdown files existed for those dates. If not, the script would adjust the date outward by another day in the relevant direction and test again, ensuring that it eventually found the closest actual previous and next files, within predefined date boundaries.
while f'{prev_date.strftime("%Y-%m-%d")}.md' not in files:
prev_date -= timedelta(days=1)
Having determined the correct previous and next dates, the script then generated and stored new strings to replace the existing links in the markdown files.
new_str = f'<< [[Daily/{prev_date.strftime("%Y-%m-%d")}|{prev_date.strftime("%a %d %B %Y")}]] | [[Daily/{next_date.strftime("%Y-%m-%d")}|{next_date.strftime("%a %d %B %Y")}]] >>'
Modifying the Files
To make the changes, the script needed to find the correct line to replace in each file. It read each file line by line until it found one that started with '<< [[Daily',
the prefix for the link line. If such a line was found, it replaced that line with the new string. In some older files this did not exist, but the line starting with *Daily Note*
did. So, it added the new line underneaths. If neither line was found, the line was to be appended the new line at the end of the file though, in practice, none of the files matched this last pattern.
# Check line exists
daily_line_index = None
for index, line in enumerate( lines ):
if line.startswith( '<< [[Daily' ):
daily_line_index = index
break
elif line.startswith( '*Daily Note*' ):
daily_line_index = index + 1
if daily_line_index is not None:
lines[daily_line_index] = new_str + '\n'
else:
# Catch-all: append it to end of file - if either of the conditions are not met.
lines.append( new_str + '\n' )
Finally, the script wrote the updated content back into each file. To track the changes made, it logged all the changes into a separate 'output.tsv'
(tab-separated values) file.
with open(f'{dir_path}/{file}', 'w') as f:
f.write('\n'.join(new_content))
Conclusion
This Python script offered a reliable solution to a significant issue with the way I (and others) have implemented Obsidian’s Daily Note feature, making chronologically linear navigation through notes seamless once again, even when some daily notes are missing.
This is also a testament to Python’s versatility, showing how it can be used for tasks beyond traditional programming or data science use-cases, serving as a powerful tool for personal productivity and problem-solving.
For the full script and instructions on how to use it, please visit my GitHub repository.