Skip to main content

notion

Read and write Notion pages, databases (data sources), and blocks via the Notion REST API.

Source facts

Repository
AtomicBot-ai/Atomic-Chat
Last source activity
July 24, 2026 at 17:52
Detected SKILL.md language
English
Stars
1,914
Forks
222

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
notion
description
Read and write Notion pages, databases (data sources), and blocks via the Notion REST API.
version
1.3.1
requires_tools
["os.shell.run","os.http.request"]
dangerous
false
platforms
["darwin","linux"]
# notion Talk to the Notion API (`api.notion.com`) directly over HTTP. No SDK required. ## Calling convention (critical) `os.http.request` is **approval-gated** in atomic-agent. Every call MUST be emitted as a **solo step** — a length-1 array, never combined with another tool in the same step: DO — solo step, GET shape (use for `/v1/pages/{id}`, `/v1/blocks/{id}/children`, `/v1/users`): ``` [ { "tool": "os.http.request", "args": { "method": "GET", "url": "https://api.notion.com/v1/pages/{page_id}", "headers": { "Authorization": "Bearer <key>", "Notion-Version": "2025-09-03" } } } ] ``` DO — solo step, POST shape (use for `/v1/search`, `/v1/pages` create, `/v1/data_sources/{id}/query`): ``` [ { "tool": "os.http.request", "args": { "method": "POST", "url": "https://api.notion.com/v1/search", "headers": { "Authorization": "Bearer <key>", "Notion-Version": "2025-09-03", "Content-Type": "application/json" }, "body": { "query": "page title" } } } ] ``` DON'T — these are rejected by the runtime with `GrammarError: approval-gated tool 'os.http.request' is forbidden inside a batch`: ``` [ { "tool": "os.http.request", ... }, { "tool": "os.http.request", ... } ] // two HTTP calls in one step [ { "tool": "reply", ... }, { "tool": "os.http.request", ... } ] // reply + HTTP in one step [ { "tool": "os.http.request", ... }, { "tool": "os.shell.run", ... } ] // any other approval-gated peer ``` To fetch N pages, emit **N consecutive solo steps**, each as `[{...}]`. Wait for each `tool_result` before issuing the next call. Do not try to parallelise reads of the Notion API. `args.body` for POST must be a **JSON object** (the runtime serialises it). Do **not** wrap it in quotes and escape the braces — passing `body: "{\"key\":\"val\"}"` works but is fragile and is the #1 cause of `400 invalid_json` from Notion. ## Setup check (lazy — read the key once, don't re-probe) Read `NOTION_API_KEY` from the environment **once** when you first need it this conversation (`printenv NOTION_API_KEY`), then reuse the value in `Authorization: Bearer …` headers for the rest of the session. Do not re-probe before every HTTP call. Map failures: - the key is empty / unset (printenv exits non-zero or prints nothing) → **Setup playbook → "NOTION_API_KEY is not set"**. - any HTTP call returns `404 object_not_found` → **Setup playbook → "Page not shared with the integration"**. ## Setup playbook (when prerequisites are missing) When a check fails, the agent's job is to OFFER concrete help and EXECUTE the fix itself — not to dump setup instructions on the user. Use this dialogue shape: 1. State plainly what is missing (one short reply). 2. Offer the most direct remediation the agent can perform via tools. 3. Wait for the user's reply (yes/no, or pasted secret). 4. Execute the fix via tools (the runtime approval gate will surface writes for confirmation). 5. Retry the original request; only then proceed. ### NOTION_API_KEY is not set The agent CAN write the key into `~/.atomic-agent/.env` itself once the user provides it. Offer two paths in a single reply: > "You don't have `NOTION_API_KEY` set. Two options: > (a) If you already have a key — send it here, I'll append it to `~/.atomic-agent/.env` and ask you to restart the agent. > (b) If you don't have a key — I'll explain how to create an integration in Notion (it takes ~1 minute), and at the end you'll send me the key and I'll append it myself. > Which do you choose?" #### Path (a) — user pastes the key When the user replies with a string starting with `ntn_` or `secret_`, validate the shape (length ≥ 40, no whitespace) and append to the env file: ``` [{ "tool": "os.fs.write", "args": { "path": "~/.atomic-agent/.env", "mode": "append", "content": "\nNOTION_API_KEY=<pasted-key>\n" } }] ``` Then reply: "The key is saved to `~/.atomic-agent/.env`. Restart the agent (Ctrl+C and start again) so the env variable is picked up — I'll continue from the same place after the restart." Do NOT echo the key back verbatim in subsequent replies; treat it as a secret from this point. #### Path (b) — guided walkthrough The agent CANNOT create Notion integrations itself (no Notion API for that — it requires browser-based OAuth-flow). Open the page for the user and walk them through the irreducible manual steps: ``` [{ "tool": "os.shell.run", "args": { "cmd": "open", "args": ["https://www.notion.so/profile/integrations"] } }] ``` Then reply with three short bullets: > "I opened the integrations page. Do three steps: > 1. Click \"+ New integration\", choose \"Internal\", give it any name, click Save. > 2. On the Configuration tab, copy the \"Internal Integration Token\" (starts with `ntn_`). > 3. Send the token here — I'll take it from there." When the user pastes the token, switch to Path (a). ### Page not shared with the integration Notion returns `404 object_not_found` when the integration doesn't have access to a page or database. The agent CANNOT add the integration via API (Notion does not expose the connection-grant endpoint to integrations). Help the user share, but do it concretely — open the page in their browser: ``` [{ "tool": "os.shell.run", "args": { "cmd": "open", "args": ["https://www.notion.so/<page_id_without_dashes>"] } }] ``` Then reply: > "I opened the page in your browser. In the top-right corner click `…` → `Connections` (or `Connect to`) → choose your integration. After that say \"done\" — I'll retry the request." ## Calling the API ``` os.http.request { method: "GET", url: "https://api.notion.com/v1/...", headers: { "Authorization": "Bearer ntn_xxx_your_key_here", "Notion-Version": "2025-09-03", "Content-Type": "application/json" } } ``` `os.http.request` only supports `GET` and `POST`. For `PATCH` (update page, append blocks) and `DELETE`, fall back to `os.shell.run` with `curl`: ``` os.shell.run { cmd: "curl", args: [ "-fsSL", "-X", "PATCH", "-H", "Authorization: Bearer ntn_xxx_your_key_here", "-H", "Notion-Version: 2025-09-03", "-H", "Content-Type: application/json", "-d", "{\"properties\": {...}}", "https://api.notion.com/v1/pages/{page_id}" ] } ``` Default `http.approvalMode` is `writes`: `GET` goes through silently, `POST`/`os.shell.run` always require approval — present a clear `preview` of what is being changed. ## Common operations All examples assume the headers from Step 1 above are attached. ### HTTP method cheat sheet | Method | Endpoints | |---|---| | `GET` | `/v1/pages/{id}`, `/v1/blocks/{id}/children`, `/v1/databases/{id}`, `/v1/data_sources/{id}`, `/v1/users`, `/v1/users/{id}` | | `POST` | `/v1/search`, `/v1/pages` (create), `/v1/data_sources/{id}/query`, `/v1/databases` (create) | | `PATCH` (curl) | `/v1/pages/{id}` (update properties), `/v1/blocks/{id}/children` (append blocks), `/v1/blocks/{id}` (update block) | | `DELETE` (curl) | `/v1/blocks/{id}` (archive block) | If a 400 comes back from Notion, **first** check that the method matches this table — picking GET for a POST endpoint is the most common mistake. ### Search (POST) > `/v1/search` is **POST-only**. There is no GET variant — `GET /v1/search` and `GET /v1/search?query=…` both return `400`. The query string and any filters go in the **JSON body**, not the URL. #### Body field reference (every field is optional) | Field | Type | Allowed shape | |---|---|---| | `query` | string | Free-text match against page/database titles. Pass `""` or omit to list everything. | | `filter` | object | **Exactly two keys**: `{ "property": "object", "value": "page" \| "data_source" }`. Property-predicate filters (`{property:"Status", select:{equals:"…"}}`) are **rejected** here — that shape belongs to `/v1/data_sources/{id}/query`. | | `sort` | object **(singular, not array)** | **Exactly two keys**: `{ "direction": "ascending" \| "descending", "timestamp": "last_edited_time" }`. Array form (`[{...}]`) is **rejected** with `body.sort should be an object or undefined`. The inner key is `timestamp`, NOT `property`. The only legal value for `timestamp` is `"last_edited_time"`. Direction must be the full word — never `"asc"` / `"desc"`. | | `page_size` | integer | `1..100`. Default `100`. | | `start_cursor` | string | Opaque cursor from the previous response's `next_cursor`. | #### Search vs database-query: side-by-side schema `/v1/search` and `/v1/data_sources/{id}/query` look similar but have **different** filter and sort shapes. Don't cross-pollinate them. | | `POST /v1/search` | `POST /v1/data_sources/{id}/query` | |---|---|---| | Sort key name | `sort` (singular) | `sorts` (plural) | | Sort value shape | object `{ direction, timestamp }` | array `[ { property, direction }, … ]` | | Sort inner key | `timestamp: "last_edited_time"` | `property: "<your-column-name>"` | | Filter shape | `{ property: "object", value: "page"\|"data_source" }` | property-predicate, e.g. `{ property: "Status", select: { equals: "Done" } }` | Search by query text (filter optional): ``` os.http.request { method: "POST", url: "https://api.notion.com/v1/search", headers: { ... }, body: { "query": "page title" } } ``` List all pages the integration can see, newest first, no query: ``` os.http.request { method: "POST", url: "https://api.notion.com/v1/search", headers: { ... }, body: { "filter": { "property": "object", "value": "page" }, "sort": { "direction": "descending", "timestamp": "last_edited_time" }, "page_size": 50 } } ``` To page through results, send the `next_cursor` from the previous response back as `start_cursor`. To list databases instead of pages, set `filter.value` to `"data_source"`. ### Get page metadata (GET) ``` GET https://api.notion.com/v1/pages/{page_id} ``` ### Get page content / blocks (GET) ``` GET https://api.notion.com/v1/blocks/{page_id}/children ``` ### Create a page in a database (POST) ``` os.http.request { method: "POST",
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub