🚀 Executive Summary

TL;DR: This guide provides a Python script to automate the migration of Notion database content to local Markdown files, addressing concerns about data ownership, offline access, and performance with Notion’s cloud-exclusive nature. It enables users to seamlessly transition their documentation and project tracking to local-first alternatives like AppFlowy, ensuring data control and accessibility.

🎯 Key Takeaways

  • Notion API integration requires creating an ‘Internal Integration’ and explicitly sharing the target database with it, using a `POST` request to query database contents.
  • Securely manage Notion API keys and database IDs by storing them in a `config.env` file and loading them programmatically, avoiding hardcoding sensitive credentials directly in scripts.
  • For large Notion databases, implement pagination using the `start_cursor` parameter and consider adding `time.sleep` to mitigate API rate limits (e.g., 3 requests per second) when fetching extensive content or block data.

Moving from Notion to AppFlowy (Local-first Alternative)

Hey there, Darian here. As a DevOps engineer, I live and breathe automation and efficiency. For years, my team and I relied on Notion for documentation and project tracking. It’s powerful, but I always felt a little uneasy about my data living exclusively on someone else’s server. After a few slowdowns during critical moments and the desire for true offline access, I started looking for a local-first alternative. That’s when I found AppFlowy.

The only hurdle was getting my years of work out of Notion and into a usable format. I used to do this manually, copying and pasting pages, until I realized I was wasting a solid couple of hours every month. This script is the solution I built—a straightforward way to pull data from a Notion database and format it for easy import into AppFlowy or any other Markdown-based tool. Let’s walk through it.

Prerequisites

Before we dive in, make sure you have the following ready:

  • A Notion account with a database you want to export.
  • A Notion API Token. You can get this by creating a new “Internal Integration” in your Notion settings.
  • The ID of the Notion Database you want to access.
  • Python 3.9+ installed on your machine.
  • AppFlowy installed to see the final result.

The Guide: From Notion API to Local Files

Step 1: Setting Up Your Workspace

First, let’s get our project environment organized. Go ahead and create a new directory for this project on your local machine. I’ll skip the standard virtualenv setup since you likely have your own workflow for that. The main thing is to install the `requests` library, which we’ll use to communicate with the Notion API. You can typically do this with a `pip install requests` command.

Inside your project directory, create two files: `export_script.py` for our code and `config.env` to store our secrets. Never hardcode secrets in your scripts!

Your `config.env` file should look like this:

NOTION_API_KEY=your_secret_api_key_here
NOTION_DATABASE_ID=your_database_id_here

Remember to also “share” your Notion database with the integration you created to get the API key. This is a common step people miss.

Step 2: Loading Configuration in Python

In `export_script.py`, our first task is to securely load the credentials from `config.env`. This simple function will parse the file and return our secrets as a dictionary. It’s a lightweight way to manage configuration without extra dependencies.

import requests
import json
import os
import re

def load_config(filename='config.env'):
    config = {}
    try:
        with open(filename, 'r') as f:
            for line in f:
                if '=' in line:
                    key, value = line.strip().split('=', 1)
                    config[key] = value
    except FileNotFoundError:
        print(f"Error: {filename} not found. Please create it.")
        return None
    return config

Step 3: Querying the Notion Database

Now for the core logic. We’ll build a function to connect to the Notion API, authenticate, and query the database. The Notion API uses a `POST` request to query a database, which might seem a bit odd, but it allows for more complex filters and sorting in the request body. For now, we’ll just fetch everything.

Pro Tip: By default, the Notion API returns a maximum of 100 pages per request. For larger databases, you’ll need to implement pagination using the `start_cursor` parameter in subsequent requests. I’ve kept this script simple to focus on the core concept, but in my production setups, I have a `while` loop that keeps fetching pages until the `has_more` property in the response is `false`.

def fetch_notion_database(database_id, api_key):
    api_url = f"https://api.notion.com/v1/databases/{database_id}/query"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
        "Notion-Version": "2022-06-28"
    }
    
    response = requests.post(api_url, headers=headers)
    
    if response.status_code != 200:
        print(f"Error fetching data: {response.status_code}")
        print(response.text)
        return None
        
    return response.json()

Step 4: Parsing the Data and Saving to Markdown

The response from Notion is a structured JSON object. Our job is to parse it, pull out the data we care about, and format it into something AppFlowy can easily understand: Markdown files. Each page in our Notion database will become a separate `.md` file.

This function handles that process. It extracts the page title (which is annoyingly nested in Notion’s structure) and a “Status” property. It then creates a directory called `output` and saves each page as a Markdown file.

def process_and_save_pages(data):
    if not data or 'results' not in data:
        print("No data to process.")
        return

    # Create an output directory if it doesn't exist
    # In a real shell, you'd just run 'mkdir -p output'
    if not os.path.exists('output'):
        os.makedirs('output')

    for page in data['results']:
        properties = page['properties']
        page_title = "Untitled"
        status = "No Status"

        # Safely extract title
        if 'Name' in properties and properties['Name']['title']:
            page_title = properties['Name']['title'][0]['plain_text']
        
        # Safely extract status
        if 'Status' in properties and properties['Status']['select']:
            status = properties['Status']['select']['name']

        # Sanitize the title to use as a filename
        safe_filename = re.sub(r'[\\/*?:"<>|]', "", page_title)
        filepath = os.path.join('output', f"{safe_filename}.md")

        # Create Markdown content with frontmatter-style properties
        content = f"---\nstatus: {status}\n---\n\n# {page_title}\n\n"
        content += "Content from this page would go here.\n"
        content += "(Fetching page block content requires additional API calls per page)."

        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(content)

    print(f"Successfully processed and saved {len(data['results'])} pages to the 'output' directory.")

Step 5: Putting It All Together

Finally, let’s create a `main` function to orchestrate the whole process. This ties everything together: load config, fetch data, process, and save.

def main():
    config = load_config()
    if not config:
        return

    api_key = config.get('NOTION_API_KEY')
    database_id = config.get('NOTION_DATABASE_ID')

    if not api_key or not database_id:
        print("API key or Database ID is missing from config.env.")
        return
        
    print("Fetching data from Notion...")
    data = fetch_notion_database(database_id, api_key)
    
    if data:
        print("Processing pages and saving to Markdown...")
        process_and_save_pages(data)
        print("Export complete.")

if __name__ == "__main__":
    main()

Now, you can run the script from your terminal (`python3 export_script.py`), and you should see a new `output` folder filled with your Notion pages as Markdown files. You can then drag and drop these directly into AppFlowy.

Common Pitfalls

Here are a few places I usually trip up when I set this up for a new project:

  • Forgetting to Share the Database: You absolutely must share the database with your integration in Notion’s UI. If you get a 401 or 404 error, this is the first thing to check.
  • Handling Different Property Types: My example only handles ‘Title’ and ‘Select’ properties. Notion has dozens of types (`multi_select`, `date`, `person`, etc.). You’ll need to expand the `process_and_save_pages` function to handle the specific properties in your database. The logic is similar: check for the property and parse its specific structure.
  • API Rate Limits: If you’re exporting thousands of pages and also fetching block content for each one, you might hit Notion’s rate limit (around 3 requests per second). The simple fix is to add a small `time.sleep(0.5)` inside your loop.

Conclusion

And that’s it. This script is a solid foundation for taking back control of your data. You now have a repeatable, automated way to pull information out of Notion and into a local, open-source format. For me, the peace of mind that comes with a local-first tool like AppFlowy is invaluable. My data is mine, it’s fast, and it works offline. I’ve even set this script to run on a weekly schedule (`0 2 * * 1 python3 export_script.py`) just to ensure I always have a fresh backup.

Feel free to adapt this script to your own needs. Hope it saves you some time.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why should I consider moving my data from Notion to a local-first alternative like AppFlowy?

Moving to a local-first tool like AppFlowy provides true data ownership, offline access, and improved performance, addressing concerns about data residing exclusively on third-party servers and potential slowdowns experienced with Notion.

âť“ How does this automated migration script compare to manual Notion export methods?

This Python script offers a repeatable and efficient automated solution for exporting Notion database pages to Markdown, significantly reducing the time and effort compared to manual copy-pasting, especially for large or frequently updated datasets.

âť“ What is a common implementation pitfall when setting up the Notion API export script?

A common pitfall is forgetting to ‘share’ the specific Notion database with the created ‘Internal Integration’ in Notion’s UI. This oversight often results in 401 or 404 API errors during data fetching.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading