notion

/home/avalon/.hermes/skills/productivity/notion/SKILL.md · raw

Notion

Talk to Notion two ways. Same integration token works for both — pick by what's available.

ntn CLI — Notion's official CLI. Shorter syntax, one-line file uploads, required for Workers. macOS + Linux only as of May 2026 (Windows support "coming soon"). Default when installed.HTTP + curl — works everywhere including Windows. Default fallback when ntn isn't installed.

Setup

1. Get an integration token (required for both paths)

  1. Create an integration at https://notion.so/my-integrations
  2. Copy the API key (starts with ntn_ or secret_)
  3. Store in ~/.hermes/.env: NOTION_API_KEY=ntn_your_key_here
  4. Share target pages/databases with the integration in Notion: page menu ...Connect to → your integration name. Without this, the API returns 404 for that page even though it exists.

2. Install ntn (preferred path on macOS / Linux)

# Recommended
curl -fsSL https://ntn.dev | bash

# Or via npm (needs Node 22+, npm 10+)
npm install --global ntn

ntn --version    # verify

Skip ntn login — use the integration token instead. This works headlessly, no browser needed:

export NOTION_API_TOKEN=$NOTION_API_KEY      # ntn reads NOTION_API_TOKEN
export NOTION_KEYRING=0                       # don't try to use the OS keychain

Add those exports to your shell profile (or to ~/.hermes/.env) so every session inherits them.

3. Choose path at runtime

if command -v ntn >/dev/null 2>&1; then
  # use ntn
else
  # fall back to curl
fi

Windows users: skip step 2 entirely until native ntn ships — Path B works fine. If you want CLI ergonomics now, install ntn inside WSL2.

API Basics

Notion-Version: 2025-09-03 is required on all HTTP requests. ntn handles this for you. In this version, what users call "databases" are called data sources in the API.

Path A — ntn CLI (preferred, macOS / Linux)

Raw API calls (shorthand for curl)

ntn api v1/users                                  # GET
ntn api v1/pages parent[page_id]=abc123 \         # POST with inline body
  properties[title][0][text][content]="Notes"
ntn api v1/pages/abc123 -X PATCH archived:=true   # PATCH; := is non-string (bool/num/null)

Syntax notes: - key=value — string fields - key[nested]=value — nested object fields - key:=value — typed assignment (booleans, numbers, null, arrays)

ntn api v1/search query="page title"

Read page metadata

ntn api v1/pages/{page_id}

Read page as Markdown (agent-friendly)

ntn api v1/pages/{page_id}/markdown

Read page content as blocks

ntn api v1/blocks/{page_id}/children

Create page from Markdown

ntn api v1/pages \
  parent[page_id]=xxx \
  properties[title][0][text][content]="Notes from meeting" \
  markdown="# Agenda

- Q3 roadmap
- Hiring"

Patch a page with Markdown

ntn api v1/pages/{page_id}/markdown -X PATCH \
  markdown="## Update

Shipped the prototype."

Query a database (data source)

ntn api v1/data_sources/{data_source_id}/query -X POST \
  filter[property]=Status filter[select][equals]=Active

For complex queries with sorts, multiple filter clauses, or compound logic, pipe JSON in:

echo '{"filter": {"property": "Status", "select": {"equals": "Active"}}, "sorts": [{"property": "Date", "direction": "descending"}]}' | \
  ntn api v1/data_sources/{data_source_id}/query -X POST --json -

File uploads (one-liner — biggest CLI win)

ntn files create < photo.png
ntn files create --external-url https://example.com/photo.png
ntn files list

Compare to the 3-step HTTP flow (create upload → PUT bytes → reference).

Useful env vars

Var Effect
NOTION_API_TOKEN Auth token (overrides keychain) — set this to your integration token
NOTION_KEYRING=0 File-based creds at ~/.config/notion/auth.json instead of OS keychain
NOTION_WORKSPACE_ID Skip the workspace picker prompt

Path B — HTTP + curl (cross-platform, default on Windows)

All requests share this pattern:

curl -s -X GET "https://api.notion.com/v1/..." \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json"

On Windows the curl shipped with Windows 10+ works as-is. PowerShell users can also use Invoke-RestMethod.

Search

curl -s -X POST "https://api.notion.com/v1/search" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{"query": "page title"}'

Read page metadata

curl -s "https://api.notion.com/v1/pages/{page_id}" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03"

Read page as Markdown (agent-friendly)

Easier to feed to a model than block JSON.

curl -s "https://api.notion.com/v1/pages/{page_id}/markdown" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03"

Read page content as blocks (when you need structure)

curl -s "https://api.notion.com/v1/blocks/{page_id}/children" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03"

Create page from Markdown

POST /v1/pages accepts a markdown body param.

curl -s -X POST "https://api.notion.com/v1/pages" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{
    "parent": {"page_id": "xxx"},
    "properties": {"title": [{"text": {"content": "Notes from meeting"}}]},
    "markdown": "# Agenda\n\n- Q3 roadmap\n- Hiring\n\n## Decisions\n- Ship MVP Friday"
  }'

Patch a page with Markdown

curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}/markdown" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{"markdown": "## Update\n\nShipped the prototype."}'

Create page in a database (typed properties)

curl -s -X POST "https://api.notion.com/v1/pages" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{
    "parent": {"database_id": "xxx"},
    "properties": {
      "Name": {"title": [{"text": {"content": "New Item"}}]},
      "Status": {"select": {"name": "Todo"}}
    }
  }'

Query a database (data source)

curl -s -X POST "https://api.notion.com/v1/data_sources/{data_source_id}/query" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": {"property": "Status", "select": {"equals": "Active"}},
    "sorts": [{"property": "Date", "direction": "descending"}]
  }'

Create a database

curl -s -X POST "https://api.notion.com/v1/data_sources" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{
    "parent": {"page_id": "xxx"},
    "title": [{"text": {"content": "My Database"}}],
    "properties": {
      "Name": {"title": {}},
      "Status": {"select": {"options": [{"name": "Todo"}, {"name": "Done"}]}},
      "Date": {"date": {}}
    }
  }'

Update page properties

curl -s -X PATCH "https://api.notion.com/v1/pages/{page_id}" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{"properties": {"Status": {"select": {"name": "Done"}}}}'

Append blocks to a page

curl -s -X PATCH "https://api.notion.com/v1/blocks/{page_id}/children" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{
    "children": [
      {"object": "block", "type": "paragraph", "paragraph": {"rich_text": [{"text": {"content": "Hello from Hermes!"}}]}}
    ]
  }'

File uploads (3-step flow)

# 1. Create upload
curl -s -X POST "https://api.notion.com/v1/file_uploads" \
  -H "Authorization: Bearer $NOTION_API_KEY" \
  -H "Notion-Version: 2025-09-03" \
  -H "Content-Type: application/json" \
  -d '{"filename": "photo.png", "content_type": "image/png"}'

# 2. PUT bytes to the upload_url returned above
curl -s -X PUT "{upload_url}" --data-binary @photo.png

# 3. Reference {file_upload_id} in a page/block payload

Property Types

Common property formats for database items:

API Version 2025-09-03 — Databases vs Data Sources

Notion Workers (advanced, requires ntn)

Workers are TypeScript programs Notion hosts for you. One worker can expose any combination of: - Syncs — pull data from external APIs into a Notion database on a schedule (default 30 min). - Tools — appear as callable tools inside Notion's Custom Agents. - Webhooks — receive HTTP events from external services (GitHub, Stripe, etc.) and act in Notion.

Plan / platform gating: - CLI works on all plans. Deploying Workers requires Business or Enterprise. - ntn is macOS/Linux only as of May 2026. Windows users need WSL2 or to wait for native support. - Free through August 11, 2026; metered on Notion credits after.

Minimal Worker

ntn workers new my-worker      # scaffold
cd my-worker
# Edit src/index.ts
ntn workers deploy --name my-worker

src/index.ts:

import { Worker } from "@notionhq/workers";

const worker = new Worker();
export default worker;

worker.tool("greet", {
  title: "Greet a User",
  description: "Returns a friendly greeting",
  inputSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] },
  execute: async ({ name }) => `Hello, ${name}!`,
});

Webhook capability

worker.webhook("onGithubPush", {
  title: "GitHub Push Handler",
  execute: async (events, { notion }) => {
    for (const event of events) {
      // event.body, event.rawBody (for signature verification), event.headers
      console.log("got delivery", event.deliveryId);
    }
  },
});

After deploy: ntn workers webhooks list shows the URL Notion generates. Treat that URL as a secret — anyone with it can POST events unless you add signature verification.

Worker lifecycle commands

ntn workers deploy
ntn workers list
ntn workers exec <capability-key> -d '{"name": "world"}'
ntn workers sync trigger <key>            # run a sync now
ntn workers sync pause <key>
ntn workers env set GITHUB_WEBHOOK_SECRET=...
ntn workers runs list                     # recent invocations
ntn workers runs logs <run-id>
ntn workers webhooks list

When asked to build a Worker, scaffold with ntn workers new, write the code in src/index.ts, set any secrets with ntn workers env set, and deploy. Notion's docs at https://developers.notion.com/workers cover the full API surface.

Notion-Flavored Markdown (used by /markdown endpoints)

Standard CommonMark plus XML-like tags for Notion-specific blocks. Use tabs for indentation.

Blocks beyond CommonMark:

<callout icon="🎯" color="blue_bg">
    Ship the MVP by **Friday**.
</callout>

<details color="gray">
<summary>Toggle title</summary>
    Children indented one tab
</details>

<columns>
    <column>Left side</column>
    <column>Right side</column>
</columns>

<table_of_contents color="gray"/>

Inline: - Mentions: <mention-user url="..."/>, <mention-page url="...">Title</mention-page>, <mention-date start="2026-05-15"/> - Underline: <span underline="true">text</span> - Color: <span color="blue">text</span> or block-level {color="blue"} on the first line - Math: inline $x^2$, block $$ ... $$ - Citations: [^https://example.com]

Colors: gray brown orange yellow green blue purple pink red, plus *_bg variants for backgrounds.

Headings 5/6 collapse to H4. Multiple > lines render as separate quote blocks — use <br> inside a single > for multi-line quotes.

Choosing the Right Path

Task mac / Linux Windows
Read/write pages, search, query databases ntn api ... curl
Read a page for an agent to summarize ntn api v1/pages/{id}/markdown curl /markdown endpoint
Upload a file ntn files create < file 3-step HTTP flow
One-off API exploration ntn api ... curl
Build a sync / webhook / agent tool hosted by Notion ntn workers ... WSL2 + ntn workers ...

Notes