Automating Obsidian with Github Actions
January 15, 2025
I use Obsidian for taking daily notes, usually in the form of lists such as ideas, goals and thoughts. These lists typically start with a hashtag and then the list underneath so that each page is linked together on the Graph by tags.
![[Pasted image 20250115123846.png]] Green dots are tags, White are pages.
This works well if I want to keep going back to the graph, finding the #goals tag, and then clicking through each page to update a goal I've achieved. However, that's not what I want to do so I devised a quick and painless way of organising my on-going hashtags into their own file regardless of the daily note they're in.
Editor's Note I have Obsidian setup to create a new file every day in a folder, as well as automatic backups to a Github repo.
The Idea
As Obsidian is just a series of folders and markdown files, the general idea was to create a script that could scan all the markdown files in my notes directory for regex patterns, then extract list items underneath the matching hashtags to a separate file.
This script could then be run automatically each night using a Github Action and any changes recommitted to the Repo to be pulled down the next time I opened my notes.
![[Pasted image 20250115125336.png]]
Execution
For the script, I went with node as it's the most convenient to get setup and running with. Additionally, I set a rule of using no 3rd-party packages as I didn't want node-related files clogging up the root directory (package.json etc).
So, I created a new folder called Scripts and within it a new Javascript file called OrganiseDaily.js.
const fs = require('fs');
const path = require('path');
/**
 * Get the previous date in the format 'dd-mm-yyyy'
 * @returns {string} The previous date in the format 'dd-mm-yyyy'
 */
function getPreviousDate() {
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  const year = yesterday.getFullYear();
  const month = String(yesterday.getMonth() + 1).padStart(2, "0");
  const day = String(yesterday.getDate()).padStart(2, "0");
  return `${day}-${month}-${year}`;
 }
/**
 * Extracts checkboxes under hashtags from the content.
 * @param {String} content
 * @returns {Object} An object with hashtags as keys and an array of checkboxes as values
 */
function extractHashtagCheckboxes(content) {
 const results = {};
 const lines = content.split('\n');
Â
 let currentHashtag = null;
 for (const line of lines) {
  const hashtagMatch = line.match(/^#(\w+)/);
  const checkboxMatch = line.match(/^-\s\[(.| )\]\s(.*)/);
  if (hashtagMatch) {
   currentHashtag = hashtagMatch[1]; // Capture the hashtag (e.g., `goals`)
   if (!results[currentHashtag]) results[currentHashtag] = [];
  } else if (currentHashtag && checkboxMatch) {
   results[currentHashtag].push(line.trim()); // Capture the checkbox line
  } else if (line.trim() === '') {
   currentHashtag = null; // Reset if an empty line is found
  }
 }
 return results;
}
/**
 * Appends checkboxes to respective hashtag files.
 * @param {Array} results
 * @param {String} rootDir
 * @param {String} date
 */
function appendToHashtagFiles(results, rootDir, date) {
 for (const [hashtag, checkboxes] of Object.entries(results)) {
  if (checkboxes.length === 0) continue;
  const filePath = path.join(rootDir, 'Tags', `${hashtag}.md`);
  const header = `### ${date}\n`; // Group under the date
  const content = header + checkboxes.join('\n') + '\n\n';
  fs.appendFileSync(filePath, content, 'utf8');
 }
}
/**
 * Processes the previous date's log file
 * @returns {void}
 */
function main() {
 const rootDir = process.cwd();
 const previousDate = getPreviousDate();
 const logFilePath = path.join(rootDir, 'Notes', new Date().getFullYear().toString(), `${previousDate}.md`);
 if (!fs.existsSync(logFilePath)) {
  console.log(`Log file for previous date (${previousDate}) does not exist.`);
  return;
 }
Â
 const content = fs.readFileSync(logFilePath, 'utf8');
 const results = extractHashtagCheckboxes(content);
 // Append extracted checkboxes to respective hashtag files
 appendToHashtagFiles(results, rootDir, previousDate);
Â
 // Calculate total new goals for reporting
 const totalGoals = results['goals'] ? results['goals'].length : 0;
 console.log(`Processed ${logFilePath} notes`);
 console.log(`Tracking ${totalGoals} new goals`);
}
main();
When run, this script gets the previous date (as I run this as midnight) and finds the date file in Notes folder.
Then it opens the file and scans through for anything that matches the # regex, before appending it to Tags/{hashtag}.md. I've also made it group hashtags by date so I can see when they were originally added.
Scheduling with Github Actions
The Github Action yaml file for this process looks just about the same as any other workflow. The action checkouts the repository, runs the script with node scripts/organiseDaily and then commits any changes back to the repo. Here's the YAML file for that:
name: Organise Tags
permissions:
contents: write
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch: # Allows manual triggering
jobs:
organise:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Organise tags
run: node scripts/organiseDaily
- name: Commit changes
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add .
git commit -m "Update hashtag files from previous day's log" || echo "No changes to commit"
- name: Push changes
run: git push
I have this scheduled to run at midnight, as well as manually for testing purposes.
And with that, my notes are one step closer to being completely organised using a bit of automation.
Thanks for reading!