BundledProductivityVersion 2.0.0

Notion Skill for Hermes Agent: API, ntn CLI, and Workers

Notion API + ntn CLI: pages, databases, markdown, Workers.

Written by Neura Market from the official Hermes Agent documentation for Notion. Commands, paths, and version numbers are reproduced from the source unchanged.

Read the official documentation

The Notion skill gives a Hermes Agent session read and write access to a Notion workspace: searching pages, pulling page bodies as Markdown, creating and patching pages, querying databases, uploading files, and, on paid plans, deploying Notion Workers. It is the skill you want when an agent needs to file meeting notes, keep a task database current, or pull a spec into context before answering a question. It is not the right tool for editing Notion's UI settings or for anything that depends on a browser session.

What it does

The skill wraps two interchangeable routes to the same Notion API. The first is ntn, Notion's official command-line client, which shortens the syntax, turns file uploads into a single command, and is the only route that can deploy Workers. The second is plain HTTP with curl, which works on every platform the skill supports. Both routes authenticate with the same integration token, so you can switch between them without reconfiguring anything.

The practical effect is that an agent can treat Notion as a file system of sorts. It can list what exists, read a page as Markdown that fits comfortably in a model context, write new content back, and query structured data out of a database. The Markdown endpoints matter more than they look: block JSON is verbose and awkward for a model to reason over, while Markdown is close to what the model already reads well.

Before you start

Get an integration token

Both routes need a Notion integration. Create one at https://notion.so/my-integrations, copy the API key (it starts with ntn_ or secret_), and store it in ${HERMES_HOME:-~/.hermes}/.env:

NOTION_API_KEY=ntn_your_key_here

The step people skip is sharing. In Notion, open the target page or database menu, choose ... → Connect to, and pick your integration by name. Until you do that, the API returns 404 for a page that plainly exists. This is the single most common cause of a first run that looks broken when it is not.

Install ntn (macOS and Linux)

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

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

ntn --version    # verify

Do not run ntn login. The integration token is enough, and it works without a browser, which is what you need in an agent runtime:

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

Put those exports in your shell profile or in ${HERMES_HOME:-~/.hermes}/.env so every session inherits them. If you skip NOTION_KEYRING=0, the CLI may try to reach the OS keychain, which is not available in a headless context.

Choose the route at runtime

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

On Windows, skip the ntn install entirely until native support ships. The curl route works as-is. If you want the CLI ergonomics now, install ntn inside WSL2.

API basics

Every HTTP request must carry Notion-Version: 2025-09-03. ntn adds it for you. In this version, the objects users call databases are called data sources in the API, which changes which endpoint you call and which ID you pass. Keep that distinction in mind for the rest of this guide.

Path A: the ntn CLI

Raw API calls

ntn api is a shorthand for curl. The same endpoints, with less ceremony:

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

The syntax has three forms worth memorising: key=value for strings, key[nested]=value for nested objects, and key:=value for typed values such as booleans, numbers, null, and arrays. The last one is what lets you send archived:=true without quoting it as a string.

Search

ntn api v1/search query="page title"

Read page metadata

ntn api v1/pages/{page_id}

Read page as Markdown

ntn api v1/pages/{page_id}/markdown

This is the endpoint to reach for when the agent needs to summarise or reason over a page. It returns Markdown rather than block JSON.

Read page content as blocks

ntn api v1/blocks/{page_id}/children

Use this when you need the structure itself, for example to find a specific block type or to walk 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

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

This is the biggest practical win of the CLI. The HTTP route needs three steps (create upload, PUT bytes, reference the upload ID); here it is one command.

Useful environment variables

VarEffect
NOTION_API_TOKENAuth token (overrides keychain), set this to your integration token
NOTION_KEYRING=0File-based creds at ~/.config/notion/auth.json instead of OS keychain
NOTION_WORKSPACE_IDSkip the workspace picker prompt

Path B: HTTP and curl

Every request follows the same shape:

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 that ships with Windows 10 and later 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

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

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

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 (three steps)

# 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

When you write to a database, each property has a shape. These are the common ones:

  • Title: {"title": [{"text": {"content": "..."}}]}
  • Rich text: {"rich_text": [{"text": {"content": "..."}}]}
  • Select: {"select": {"name": "Option"}}
  • Multi-select: {"multi_select": [{"name": "A"}, {"name": "B"}]}
  • Date: {"date": {"start": "2026-01-15", "end": "2026-01-16"}}
  • Checkbox: {"checkbox": true}
  • Number: {"number": 42}
  • URL: {"url": "https://..."}
  • Email: {"email": "user@example.com"}
  • Relation: {"relation": [{"id": "page_id"}]}

Databases versus data sources

Version 2025-09-03 split the old database concept in two. Databases became data sources, and each database now has two IDs:

  • database_id when creating pages: parent: {"database_id": "..."}
  • data_source_id when querying: POST /v1/data_sources/{id}/query

Search returns databases as "object": "data_source" with the data_source_id field. If a query fails with a not-found error, check which ID you passed before anything else.

Notion Workers

Workers are TypeScript programs that Notion hosts for you. A single worker can expose any combination of three capabilities:

  • Syncs pull data from external APIs into a Notion database on a schedule (default 30 minutes).
  • Tools appear as callable tools inside Notion's Custom Agents.
  • Webhooks receive HTTP events from external services such as GitHub or Stripe and act in Notion.

Workers are gated. The CLI works on all plans, but deploying Workers requires Business or Enterprise. ntn is macOS and Linux only as of May 2026, so Windows users need WSL2 or have to wait for native support. Workers are free through August 11, 2026, and metered on Notion credits after that.

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 who has 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 own docs at https://developers.notion.com/workers cover the full API surface.

Notion-flavored Markdown

The /markdown endpoints accept standard CommonMark plus XML-like tags for Notion-specific blocks. Indent with tabs.

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 features include mentions, underline, color, math (inline $x^2$, block $$ ... $$), and citations written as [^https://example.com]. Colors are gray brown orange yellow green blue purple pink red, plus *_bg variants for backgrounds.

Two rules catch people out. Headings 5 and 6 collapse to H4. And multiple > lines render as separate quote blocks, so use a single > with line breaks inside it for a multi-line quote.

Choosing the right path

Taskmac / LinuxWindows
Read/write pages, search, query databasesntn api ...curl
Read a page for an agent to summarizentn api v1/pages/{id}/markdowncurl /markdown endpoint
Upload a filentn files create < file3-step HTTP flow
One-off API explorationntn api ...curl
Build a sync / webhook / agent tool hosted by Notionntn workers ...WSL2 + ntn workers ...

When not to use it

The skill covers the API, not the UI. Anything that lives only in Notion's interface, such as database view filters, cannot be set through these calls. If you need streaming Notion access from inside a live session rather than one-shot calls, Notion also ships an MCP server (Notion MCP, roughly 91% more token-efficient on database operations than the previous version), which you can wire through Hermes' MCP support. For most one-shot tasks, the two paths in this guide are enough.

Limits and gotchas

  • Page and database IDs are UUIDs. Both the dashed and undashed forms are accepted.
  • The rate limit is about 3 requests per second on average. The CLI does not bypass it, so a tight loop of calls will still hit the ceiling.
  • The API cannot set database view filters. That is UI-only.
  • When creating a data source, pass "is_inline": true to embed it in a page.
  • Always pass -s to curl. It suppresses progress bars and keeps agent output clean.
  • Pipe JSON through jq when reading, for example ... | jq '.results[0].properties'.
  • A 404 on a page you can see in the browser almost always means the integration was not shared with that page.

Related

Notion's own developer documentation at https://developers.notion.com/workers is the reference for the Workers API surface. The integration management page at https://notion.so/my-integrations is where tokens are created and rotated. If you want in-session streaming access instead of one-shot calls, look at Hermes' MCP support and the Notion MCP server.

More Productivity skills