Oct 17, 2021

Updating Notion Pages on a Schedule with GitHub Actions

About five months ago, Notion launched their long-awaited public API. The team initially delivered basic features and continuously improved the surface with more functionality and block types to support a growing number of use cases.

I use Notion for, drum roll, note-taking. I create a page every day to take down whatever needs to be done and what actually happened, together with some useful checklists. Since the daily notes are in a database, I usually create a link to today's notes on the main page.

While I could mark the page as a favorite, using the dedicated link-to-page block will allow you to drag blocks into it, such as completed To Do items. Unfortunately, the Notion API does not yet support editing said block type, hopefully this is added in quite soon.

For this post and the time being, I'll demonstrate an automation that links today's notes page using scheduled GitHub actions jobs. And to make it even more exciting, I'll showcase an alternative method to trigger your workflows using the Shortcuts app on iOS.

Creating the automation

In this guide, I'll use Node.js with the official Notion SDK. Before you start, make sure to create an integration and copy the secret integration token. The great thing about using the official SDK is, that you get types out of the box. We're going to supply the token with the NOTION_TOKEN environment variable.

import { Client as NotionClient } from '@notionhq/client';
import { format } from 'date-fns';

// This data determines the page we'll pin to the main page
const dateToUse = new Date();

// Construct our notion API client instance
const notion = new NotionClient({
  auth: process.env.NOTION_TOKEN
});

We'll continue by declaring the database to query, which is our daily notes database. To get its ID, head over to the page and click on Share, then Copy link to copy the URL to your clipboard. From there, you can extract the database ID.

Our second ID is a callout block placed on the main page, which includes a link to today's notes. You can obtain a block ID by hovering over the block, clicking the menu button on the left-hand side, and once again clicking Copy link.

// https://www.notion.so/<workspace>/<database id>?v=...
const databaseId = '<Extract ID from Share -> Copy link>';

// https://www.notion.so/<workspace>/<page id>#<block id>
const calloutBlockId = '<The ID of the callout block we want to sync>';

Now that we have our IDs in place, we can start with our actual business logic. We'll start by getting the page that corresponds to the date we set previously. For this guide, we expect only one page to exist, and we will exit gracefully in case no page was created by the time the job runs. Notion's database filters allow you to find pages that match exactly what you're looking for, with many supported property types.

async function retrieveTodaysNotes(
  notion: NotionClient,
  databaseId: string,
  dateToUse: Date
) {
  const {
    results: [todaysNotes]
  } = await notion.databases.query({
    database_id: databaseId,
    // Only load pages with matching date
    filter: {
      date: { equals: format(dateToUse, 'yyyy-MM-dd') },
      property: 'Date'
    },
    // We only consider one page
    page_size: 1
  });
  if (!todaysNotes) {
    // If no notes were created yet, ignore
    return null;
  }

  // Retrieve the title of today's notes page
  const todayNotesTitle = todaysNotes.properties['Name'];
  if (todayNotesTitle.type !== 'title') {
    throw new Error('Missing title property');
  }

  return {
    title: todayNotesTitle.title[0].plain_text,
    url: todaysNotes.url
  };
}

The next utility function will be to update our callout block to today's page title and URL. We do this with a second API call that updates the callout block in place. This way of updating is nice because it will not break the layout unintentionally, as you specify exactly which block you want to change. Moving the block should be handled automatically as well.

async function updateCallout(
  notion: NotionClient,
  calloutBlockId: string,
  title: string,
  url: string
) {
  await notion.blocks.update({
    block_id: calloutBlockId,
    type: 'callout',
    callout: {
      text: [
        {
          type: 'text',
          text: {
            content: title,
            link: {
              url
            }
          }
        }
      ],
      icon: {
        type: 'emoji',
        emoji: 'πŸ—’οΈ'
      }
    }
  });
}

With all functions in place, we can finally put things together. We'll retrieve today's notes and update the callout, then exit.

async function main() {
  const todaysNotes = await retrieveTodaysNotes(notion, databaseId, dateToUse);
  if (!todaysNotes) {
    console.log('No notes created yet, nothing to do here');
    return;
  }

  await updateCallout(
    notion,
    calloutBlockId,
    todaysNotes.title,
    todaysNotes.url
  );
}

main();

You can try running this with an example configuration, just set the NOTION_TOKEN in your environment and run the compiled JS file.

export NOTION_TOKEN="..."
yarn tsc
node ./dist/pin-daily-notes.js

Scheduling it

To run our automation on a recurring basis, we'll use the schedule event for GitHub Actions. For the time, I've set it to run at 7:00 UTC. Note that the exact local time will vary depending on your timezone and whether you're in daylight saving time.

on:
  schedule:
    - cron:  '0 7 * * *'

With our schedule set, we can add the remaining workflow content, including steps to set up the environment, install and cache dependencies, and build and start the script. We'll also add a workflow_dispatch trigger for manual invocation of the workflow.

name: Pin Daily Notes
on:
  schedule:
    - cron:  '0 7 * * *'
  workflow_dispatch:
env:
  NOTION_TOKEN: ${{Β secrets.NOTION_TOKEN }}
jobs:
  pin-daily-notes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '16'
          cache: 'yarn'
      - run: yarn install
      - run: yarn build
      - run: yarn start

Extra: Manual Dispatch via Shortcuts

Now that our job runs on a schedule, what happens if you wake up just a bit too late or create the page after the job has already run and exited gracefully? You can use the workflow_dispatch trigger to run the job manually on GitHub itself, but there's an even easier way to get there.

On Apple devices supporting the Shortcuts feature, we can create a new shortcut with the Dispatch Workflow action provided by the GitHub app. This action requires to set the owner, repository, branch, and workflow file name and once set, simply dispatches the workflow as you'd do on desktop.

You can even add the shortcut to your home screen or invoke it with Siri, if you wish.