DEV Community

Cover image for Syncing Obsidian Notes to Notion by Python Script (Part 2) - Create New Pages Based on Tags
koshirok096
koshirok096

Posted on

Syncing Obsidian Notes to Notion by Python Script (Part 2) - Create New Pages Based on Tags

Introduction

In the previous article, I introduced a Python script that automatically transfers notes created in Obsidian to an existing Notion database for daily logs. This allowed for a balanced workflow where notes can be casually written in Obsidian and later organized in Notion, helping to separate the acts of capturing and structuring your thoughts.

In this follow-up, I'll extend that script to categorize each note based on its tags and automatically create a new page in the appropriate Notion database. Importantly, the original feature—appending the note to the existing daily log—remains intact.

Here’s how the notes will be handled based on tags:

  • No special functional tag (e.g., only WIP or DONE): The note is appended only to the daily log (as in the previous setup)
  • Tag ToTask: Appended to the daily log and registered as a task in the Task List database
  • Tag ToFleeting: Appended to the daily log and added to the Fleeting Notes database

To give you some context: this setup is based on my actual workflow using both Obsidian and Notion. Daily reflections without tags are treated as regular journal entries, actionable items are tagged and stored in the Task DB, and fragmented ideas or early thoughts go into Fleeting Notes.

This post simplifies the concept for clarity, but the core idea is:

“All notes in Obsidian are first appended to a daily log in Notion. If specific tags are found, they are also added as new pages in the corresponding Notion database.”

As with the previous post, this is based on my own use case, but the essence of the post is about automating the creation of new Notion pages from tagged Obsidian notes using Python. So, feel free to customize this setup for your own workflow.

Image description

Review of the Setup

Let’s briefly recap the prerequisites introduced in Part 1. For detailed steps, see the previous article.

Obsidian Note Structure

Each note is saved as a Markdown file with YAML frontmatter, like this:

---
uid: 20250425150201
title: "Test Note"
tags:
  - WIP
  - ToTask
created: April 25, 2025 6:45 AM
updated: April 25, 2025 6:46 AM
---

The main content goes here.
Enter fullscreen mode Exit fullscreen mode
  • WIP: Indicates an unprocessed note. This will be replaced with DONE after processing
  • ToTask, ToFleeting: Tags used to route the note to specific Notion databases

Folder Structure (for reference)

/
├── Inbox       ← Where new notes are stored
└── Archives    ← Where processed notes are moved
Enter fullscreen mode Exit fullscreen mode

Image description

Processing Flow

The updated script performs the following steps:

  1. Read each .md file in the Obsidian Inbox folder

  2. Separate the YAML frontmatter from the note body

  3. Check for the presence of the WIP tag—if absent, skip the file

  4. Extract the date from the first 8 digits of the filename (timestamp format), and search for a matching Notion page

  5. If a matching page is found, append the note body as a toggle block at the bottom

⬇⬇⬇ New functionality added in this update ⬇⬇⬇

  1. Based on tags, optionally create a new page in either the Task List DB or the Fleeting Notes DB

  2. Once processing is complete, replace WIP with DONE and move the file to the archive folder (same as before)

This allows each note to be automatically routed and recorded based on its content and purpose.

Running the Python Script

The script from the previous article has been extended with the following features:

  • Uses pyyaml to extract tags from the YAML frontmatter

  • If the tag ToTask is present, a new page is created in the Task List database

  • If the tag ToFleeting is present, a new page is created in the Fleeting Notes database

  • Before running the script, be sure to add the following environment variables to your .env file:

TASKLIST_DB=your_task_list_database_id
FLEETING_DB=your_fleeting_notes_database_id
Enter fullscreen mode Exit fullscreen mode

For details on how to find your database ID or set up sharing permissions, refer to the previous article or consult Notion’s official API documentation.

If you haven’t installed the required Python libraries yet, set them up using:

pip install python-dotenv pyyaml requests
Enter fullscreen mode Exit fullscreen mode

📜 Full Extended Script

import os
import shutil
import requests
from datetime import datetime
from dotenv import load_dotenv
import yaml

# === Load environment variables ===
load_dotenv()

NOTION_TOKEN = os.getenv("NOTION_TOKEN")
DAILY_DB = os.getenv("DAILY_DB")
TASKLIST_DB = os.getenv("TASKLIST_DB")     # Must be defined in .env
FLEETING_DB = os.getenv("FLEETING_DB")     # Must be defined in .env
OBSIDIAN_INBOX = os.getenv("OBSIDIAN_INBOX")
OBSIDIAN_ARCHIVE = os.getenv("OBSIDIAN_ARCHIVE")

headers = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Notion-Version": "2022-06-28",
    "Content-Type": "application/json"
}

def get_target_date_from_filename(filename):
    date_obj = datetime.strptime(filename[:8], "%Y%m%d")
    return date_obj.strftime("%d/%b/%y")

def query_notion_page_by_title(database_id, target_title):
    url = f"https://api.notion.com/v1/databases/{database_id}/query"
    query = {
        "filter": {
            "property": "Name",  # This must match the database's title property name
            "title": {
                "equals": target_title
            }
        }
    }
    response = requests.post(url, headers=headers, json=query)
    if response.status_code != 200:
        print(f"[Error] Notion API error: {response.status_code} / {response.text}")
        return None
    data = response.json()
    if data.get("results"):
        return data["results"][0]["id"]
    return None

def append_toggle_to_page(page_id, uid, body):
    block = {
        "object": "block",
        "type": "toggle",
        "toggle": {
            "rich_text": [{
                "type": "text",
                "text": {"content": f"OBS_LOG : {uid}"}
            }],
            "children": [{
                "object": "block",
                "type": "paragraph",
                "paragraph": {
                    "rich_text": [{
                        "type": "text",
                        "text": {"content": body[:2000]}
                    }]
                }
            }]
        }
    }
    url = f"https://api.notion.com/v1/blocks/{page_id}/children"
    response = requests.patch(url, headers=headers, json={"children": [block]})
    return response.status_code == 200

def extract_yaml_tags(note_content):
    if note_content.startswith('---'):
        lines = note_content.splitlines()
        try:
            end_idx = lines[1:].index('---') + 1
            yaml_block = '\n'.join(lines[1:end_idx])
            data = yaml.safe_load(yaml_block)
            return data.get('tags', [])
        except Exception as e:
            print(f"[Error] YAML parse failed: {e}")
    return []

def create_page_in_db(database_id, title, content):
    url = "https://api.notion.com/v1/pages"
    payload = {
        "parent": { "database_id": database_id },
        "properties": {
            "Title": {  
                "title": [{
                    "type": "text",
                    "text": { "content": title }
                }]
            }
        },
        "children": [{
            "object": "block",
            "type": "paragraph",
            "paragraph": {
                "rich_text": [{
                    "type": "text",
                    "text": { "content": content[:2000] }
                }]
            }
        }]
    }
    response = requests.post(url, headers=headers, json=payload)
    if response.status_code != 200:
        print(f"[Error] Failed to create page: {response.status_code} / {response.text}")
    return response.status_code == 200

def main():
    for filename in os.listdir(OBSIDIAN_INBOX):
        if not filename.endswith(".md"):
            continue

        filepath = os.path.join(OBSIDIAN_INBOX, filename)
        with open(filepath, "r", encoding="utf-8") as f:
            note_content = f.read()

        if "WIP" not in note_content:
            print(f"[Skip] {filename} (no WIP tag)")
            continue

        uid = filename.split(".")[0]
        date_title = get_target_date_from_filename(filename)

        # Add content to Daily Journal
        daily_page_id = query_notion_page_by_title(DAILY_DB, date_title)
        if daily_page_id:
            success = append_toggle_to_page(daily_page_id, uid, note_content)
            if success:
                print(f"[Daily] {filename}{date_title}")

                # Tag Check
                tags = extract_yaml_tags(note_content)
                print(f"[Debug] Tags for {filename}: {tags}")

                if "ToTask" in tags:
                    if create_page_in_db(TASKLIST_DB, uid, note_content):
                        print(f"[Task] {filename} → Task DB")

                if "ToFleeting" in tags:
                    if create_page_in_db(FLEETING_DB, uid, note_content):
                        print(f"[Fleeting] {filename} → Fleeting DB")

                # Mark DONE and archive
                updated_content = note_content.replace("WIP", "DONE")
                with open(filepath, "w", encoding="utf-8") as f:
                    f.write(updated_content)

                shutil.move(filepath, os.path.join(OBSIDIAN_ARCHIVE, filename))
                print(f"[Archive] {filename} moved to Archive")
            else:
                print(f"[Error] Failed to append {filename} to Notion.")
        else:
            print(f"[Error] Daily Journal not found for {filename}")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

With this script, appending notes to your daily log, managing tasks, and capturing fleeting ideas can all be automated—allowing you to efficiently sort and organize everything you've recorded in Obsidian in one go.

Image description

Summary of Script Changes

Here’s a quick recap of what’s new in this updated version of the script:

  • Added the pyyaml library to extract tag information from the YAML frontmatter via a new extract_yaml_tags function

  • If the ToTask tag is present, a task is created in the Task List database

  • If the ToFleeting tag is present, a note is added to the Fleeting Notes database

  • If no tags are found, the note is simply appended to the log database only (as in the previous version)

The core logic of the script—including appending content to your existing note database—remains unchanged and fully intact.

Conclusion

With this updated script, all you need to do is jot down your thoughts in Obsidian—everything else, from classification to syncing with Notion, happens automatically. This greatly streamlines your workflow and reduces the time and effort needed to manage your notes.

In my own setup, I’ve added a few more custom features on top of what I covered today, such as:

  • Assigning a UID property to each created note in Notion

  • Applying different naming conventions based on the target database

  • Generating Anki-compatible CSV files automatically for notes tagged for study

However, I decided to focus this article specifically on creating new Notion entries based on tags, to keep things simple and actionable.

On a personal note, both this article and the previous one are based on systems I already had in place—but organizing everything clearly and writing it out afterward (especially as someone still learning Python and relying on AI for support) was no small task. If you find anything unclear or notice any issues, I’d genuinely appreciate your feedback. (That said, the code itself has been thoroughly tested and is working as expected.)

This workflow has made a significant difference in my daily productivity, and I hope it helps others looking to bridge Obsidian and Notion more efficiently.

In future posts, I may go into more detail about Anki integration or advanced database property handling—so stay tuned if that interests you!

Thanks for reading! 🙌

Top comments (0)