notion
Read and write Notion pages, databases (data sources), and blocks via the Notion REST API.
git clone --depth 1 https://github.com/AtomicBot-ai/atomic-agent /tmp/notion && cp -r /tmp/notion/starter-skills/notion ~/.claude/skills/notionSKILL.md
# 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.Read macOS Calendar events via the `icalBuddy` CLI and create events via AppleScript (osascript). Use to check the user's calendar, agenda, upcoming events, or add an event on macOS.
Manage Apple Notes via the `memo` CLI on macOS — create, view, search, edit, export.
Manage Apple Reminders via the `remindctl` CLI on macOS — list, add, complete, delete, manage lists.
Transcribe speech from audio files (mp3, m4a, wav, ogg, flac, webm) to text using the local `whisper` CLI — no API key. Use whenever a task hinges on the spoken content of an audio attachment.
Currency exchange rates and conversion via the Frankfurter API (no key). Use for FX rates, "convert X to Y", or historical/time-series rates.
Manage Docker containers, images, volumes, and Compose stacks via the `docker` CLI — list, inspect, logs, run, build, stop, remove, compose up/down. Use for local container ops.
Process audio and video with the `ffmpeg` / `ffprobe` CLIs — convert, trim, extract audio, resize, change format, make GIFs, inspect media. Use for any audio/video transformation.
Drive GitHub via the official `gh` CLI — repos, issues, pull requests, releases, gists, Actions runs, and raw REST through `gh api`. Use when the user asks to inspect or manage GitHub.