---
title: Notion
icon: /images/notion-logo.svg
description: Mount Notion pages as a virtual filesystem.
---
The Notion resource exposes a Notion workspace as a virtual filesystem
mounted at a prefix such as `/notion/`.

For API key setup, see [Notion Setup](/python/setup/notion).

## Config

```python
import os

from mirage import MountMode, Workspace
from mirage.resource.notion import NotionConfig, NotionResource

config = NotionConfig(api_key=os.environ["NOTION_API_KEY"])
resource = NotionResource(config=config)
ws = Workspace({"/notion": resource}, mode=MountMode.WRITE)
```

| Field      | Required | Default                        | Description             |
| ---------- | -------- | ------------------------------ | ----------------------- |
| `api_key`  | yes      |                                | Notion integration token |
| `base_url` | no       | `https://api.notion.com/v1`    | API base URL            |

## Filesystem Layout

```text
/notion/
  pages/
    <page-title>__<page-id>/
      page.json
      <child-page-title>__<child-id>/
        page.json
        ...
  databases/
    <database-title>__<database-id>/
      database.json
      <data-source-name>__<data-source-id>/
        data_source.json
        <row-page-title>__<page-id>/
          page.json
          ...
```

Example:

```text
/notion/
  pages/
    Project_Roadmap__a1b2c3d4/
      page.json
      Q1_Goals__e5f6g7h8/
        page.json
      Q2_Goals__i9j0k1l2/
        page.json
    Meeting_Notes__m3n4o5p6/
      page.json
  databases/
    Tasks__4a3b21915e77/
      database.json
      Tasks__d5000000-2222-3333-4444-555566667777/
        data_source.json
        Write_proposal__62212c5affe6/
          page.json
        Build_dashboards__f988c5a145ef/
          page.json
```

The `pages/` hierarchy mirrors Notion's standalone page tree. Each page
directory contains a `page.json` with the page metadata and content, and
child pages appear as nested directories.

The `databases/` hierarchy is one level deeper than the page tree,
because the `2025-09-03` API generation split a database into a
container plus one or more **data sources**. The column schema and the
rows both live on the data source, so a row page sits at depth 4 under
`databases/`, not 3. The name stutters for a single-source database
because Notion names the auto-created data source after its database;
that disappears the moment a database holds two.

`ls` a data source directory to enumerate its row pages.

### database.json

The container's identity. It carries `database_id`, `title`, `url`,
timestamps, `parent`, `archived`, `is_inline`, and the `data_sources`
stubs that name the directories beneath it. It **does not carry
`properties`**: at this API version the column schema lives on the data
source, and `GET /v1/databases/{id}` no longer answers with one.

```json
{
  "database_id": "eeee1111-2222-3333-4444-555566667777",
  "title": "Tasks",
  "url": "https://www.notion.so/eeee1111222233334444555566667777",
  "created_time": "2026-01-01T00:00:00.000Z",
  "last_edited_time": "2026-01-02T00:00:00.000Z",
  "parent": { "type": "workspace", "workspace": true },
  "archived": false,
  "is_inline": false,
  "data_sources": [
    { "id": "d5000000-2222-3333-4444-555566667777", "name": "Tasks" }
  ]
}
```

### data_source.json

The typed column schema (Notion's own property objects), with no rows
inline:

```json
{
  "data_source_id": "d5000000-2222-3333-4444-555566667777",
  "database_id": "eeee1111-2222-3333-4444-555566667777",
  "title": "Tasks",
  "created_time": "2026-01-01T00:00:00.000Z",
  "last_edited_time": "2026-01-02T00:00:00.000Z",
  "database_parent": { "type": "workspace", "workspace": true },
  "archived": false,
  "properties": {
    "Name": { "id": "title", "type": "title", "title": {} },
    "Priority": { "id": "pri", "type": "number", "number": { "format": "number" } },
    "Due": { "id": "du", "type": "date", "date": {} }
  }
}
```

A data source id is **not** its database id. Turn one into the other
with `ntn datasources resolve <database-id>`.

### page.json

A page, including a database row. Notion blocks render to `markdown` and
stay available raw under `blocks`; a row's cell values are under
`properties`, as Notion's own property objects, answering to the schema
in the `data_source.json` one level up:

```json
{
  "page_id": "ffff1111-2222-3333-4444-555566667777",
  "title": "Write spec",
  "url": "https://www.notion.so/ffff1111222233334444555566667777",
  "created_time": "2026-01-01T00:00:00.000Z",
  "last_edited_time": "2026-01-02T00:00:00.000Z",
  "parent_type": "data_source_id",
  "parent_id": "d5000000-2222-3333-4444-555566667777",
  "archived": false,
  "created_by": "faa6ebe0-0686-466d-96b3-2480ddda715b",
  "last_edited_by": "faa6ebe0-0686-466d-96b3-2480ddda715b",
  "properties": {
    "Name": {
      "id": "title",
      "type": "title",
      "title": [{ "type": "text", "plain_text": "Write spec" }]
    },
    "Priority": { "id": "pri", "type": "number", "number": 2 },
    "Done": { "id": "dn", "type": "checkbox", "checkbox": true }
  },
  "markdown": "",
  "blocks": []
}
```

So a row's cells are readable with the ordinary tools:

```bash
jq '.properties.Priority.number' "$ROW/page.json"
jq -r '.properties | to_entries[] | "\(.key)\t\(.value.type)"' "$ROW/page.json"
```

Blocks with children embed them recursively under a `children` key, and
the Markdown renders nested blocks with indentation. A standalone page's
`properties` holds only its title, which is what the API returns for
one.

## Cache

Uses `IndexCacheStore` for page metadata. No separate content
cache - file content caching is handled by the workspace `IOResult`
mechanism.

## Example

```python
import asyncio
import os

from dotenv import load_dotenv

from mirage import MountMode, Workspace
from mirage.resource.notion import NotionConfig, NotionResource

load_dotenv(".env.development")

config = NotionConfig(api_key=os.environ["NOTION_API_KEY"])
resource = NotionResource(config=config)


async def main():
    ws = Workspace({"/notion": resource}, mode=MountMode.WRITE)

    # List top-level pages
    r = await ws.execute("ls /notion/pages/")
    print(await r.stdout_str())

    # List shared databases, then a database's data sources, then its rows
    r = await ws.execute("ls /notion/databases/")
    print(await r.stdout_str())
    r = await ws.execute("tree -L 3 /notion/databases/")
    print(await r.stdout_str())

    # Read a page
    r = await ws.execute(
        'cat "/notion/pages/Project_Roadmap__a1b2c3d4/page.json"'
    )
    print(await r.stdout_str())

    # Read one row's cells
    r = await ws.execute(
        'jq ".properties.Priority.number" '
        '"/notion/databases/Tasks__4a3b21915e77'
        '/Tasks__d5000000-2222-3333-4444-555566667777'
        '/Write_proposal__62212c5affe6/page.json"'
    )
    print(await r.stdout_str())

    # Search across all pages
    r = await ws.execute('grep "deadline" /notion/pages/')
    print(await r.stdout_str())

    # Tree view
    r = await ws.execute("tree -L 2 /notion/")
    print(await r.stdout_str())

    # Search pages with the Notion search API
    r = await ws.execute('ntn api v1/search -d \'{"query":"Roadmap"}\'')
    print(await r.stdout_str())

    # Create a new page from Markdown
    r = await ws.execute(
        "ntn pages create --content '# New Page' --parent page:a1b2c3d4"
    )
    print(await r.stdout_str())

    # Replace an existing page's body
    r = await ws.execute(
        "ntn pages edit a1b2c3d4 --content '# Replaced body'")
    print(await r.stdout_str())


if __name__ == "__main__":
    asyncio.run(main())
```

## Shell Commands

Standard commands available on the mounted Notion tree:

| Command         | Notes                                      |
| --------------- | ------------------------------------------ |
| `ls`            | List pages and child pages                 |
| `cat`           | Read page.json content                     |
| `head` / `tail` | First/last N lines                         |
| `grep` / `rg`   | Search across pages                        |
| `jq`            | Query page JSON fields                     |
| `wc`            | Line/word/byte counts                      |
| `stat`          | File metadata                              |
| `find`          | Recursive search with `-name`, `-maxdepth` |
| `tree`          | Directory tree view                        |

Acting on Notion (creating, editing and trashing pages, querying data
sources, and every route that has no typed verb) goes through the
[ntn CLI](/python/cli/ntn) when installed. Ids are positional: use the
`<page-id>` / `<database-id>` / `<data-source-id>` from a path segment as
the operand.
