notion-cli-agent
Local CLI for full Notion access.
Binary
notion <args> # globally installed via npm
Auth — just run notion <cmd>
That's it. The CLI handles auth automatically. Do not prefix with NOTION_TOKEN=... unless notion doctor reports a 401 (see Troubleshooting below).
Token resolution order inside the CLI:
NOTION_TOKENenv varNOTION_API_KEYenv var~/.config/notion/api_key← source of truth~/.notion/token
The recommended setup keeps ~/.config/notion/api_key and the NOTION_TOKEN env var in lock-step by sourcing the file from .zshrc:
# in ~/.zshrc — single source of truth for the token
export NOTION_TOKEN="$(cat ~/.config/notion/api_key 2>/dev/null)"
With this, env var and file can't diverge — the stale-token gotcha disappears. Run notion doctor to verify (5/5 green).
Troubleshooting (only if notion doctor reports 401)
Symptom: notion doctor shows ✅ Token: Token found but ❌ API Connection: Token is invalid or expired.
Means: the env var is stale (a parent process exported an outdated token). Fix:
# One-shot bypass (uses the file directly):
NOTION_TOKEN=$(cat ~/.config/notion/api_key) notion <cmd>
# Permanent fix: update .zshrc to the dynamic export above, restart shell.
Workspace sync (do this first)
notion sync # cache all databases locally
notion list # show cached databases
notion list --json # for parsing
After sync, use database names instead of UUIDs in ALL commands:
notion db query "Tasks" --limit 5 --llm
notion find "Tasks" "overdue" --llm
notion stats overview "Projects"
If ~/.config/notion/workspace.json exists (from sync or the notion-onboarding skill), names resolve automatically. Falls back to UUIDs if no cache.
Agent Workflow
- Sync workspace —
notion sync(once per session, ornotion listif already synced) - Understand schema —
notion inspect context "Tasks"ornotion inspect schema "Tasks" --llm - Query deterministically first — prefer
db query --title,search --exact --db --first, or--llmover fuzzy workspace-wide search when you know the target DB - Write with
--dry-runfirst on bulk/batch ops, then confirm with user
Core Commands
Discover
notion sync # cache databases for name lookup
notion list # show cached databases
notion inspect ws --compact # all databases, names + ids
notion inspect schema "Tasks" --llm # property types + valid values
notion inspect context "Tasks" # workflow context + examples
notion ai prompt "Tasks" # DB-specific agent instructions
Users & guests
notion user list # members + bots + cached guests
notion user resolve-guests # discover guests, cache them
notion user get <user_id> # works for guests too (by id)
Guests are NOT in notion user list. Notion's GET /v1/users returns only
members and bots — guests are invisible — so you can't list them or look up
their id to assign them. Run notion user resolve-guests once: it walks search
results, reads every page's created_by/last_edited_by, resolves the unknown
ids via GET /v1/users/{id}, and caches the guests in
~/.config/notion/guests.json. After that, user list shows them flagged
[guest], and you can assign them in --prop "Field:people=…" by email or
name (see Property type hints). notion sync also fishes guests as a side
effect.
Query
# Exact lookup in a known DB (deterministic — uses database query API)
notion db query "Tasks" --title "Known Page" --json
notion db query "Tasks" --limit 20 --llm # compact output
# Fuzzy search (workspace-wide, best-effort — Notion may miss long titles)
notion search "keyword" --limit 10
notion search "keyword" --db <db_id> --llm # filter by parent DB
notion search "short title" --exact --first --json # best-effort exact match
# Natural language
notion find "overdue tasks unassigned" -d <db_id> --llm
notion find "high priority" -d <db_id> --explain # preview filter, don't run
For exact lookup by title in a known DB, always use db query --title — not search --exact. Notion's search API is fuzzy and may miss pages with long or common-word titles.
Read pages
notion page get <page_id> # properties
notion page get <page_id> --content # + content blocks
notion page get <page_id> --json # raw JSON
notion page read <page_id> # content as Markdown (native API, 1 call)
notion page read <page_id> --blocks # legacy block-by-block conversion
notion page read <page_id> -o page.md # save to file
notion ai summarize <page_id> # concise summary
notion ai extract <page_id> --schema "email,phone,date"
Write pages
notion page create --parent <db_id> --title "Task Name"
notion page create --parent <db_id> --title "Task" --prop "Status:status=Todo" --prop "Priority:select=High"
notion page update <page_id> --prop "Status:status=Done"
notion page update <page_id> --clear-prop "Assignee" # type-aware clear
notion page update <page_id> --clear-prop "Tags" --clear-prop "Deadline"
notion page write <page_id> -f content.md # write Markdown to page
notion page write <page_id> -f doc.md --replace # replace all content
notion page edit <page_id> --at 3 --delete 2 # surgical block editing
notion page edit <page_id> --at 5 --markdown "New text" # insert at position
Add blocks
notion block append <page_id> --text "Paragraph"
notion block append <page_id> --heading2 "Section" --bullet "Item 1" --bullet "Item 2"
notion block append <page_id> --todo "Action item"
Files & attachments
<source> = local path, public URL, or an existing file_upload ID.
notion file attach <page_id> shot.png # upload + append (type auto-detected)
notion file attach <page_id> a.pdf b.png --caption "Q3"
notion file upload shot.png # upload only → prints the ID
notion block append <page_id> --text "See below" --image shot.png
notion page update <page_id> --icon logo.png --cover https://example.com/hero.jpg
notion page update <page_id> --attach "Attachments=spec.pdf,diagram.png"
notion comment create --page <page_id> -t "Screenshot" --attach shot.png # max 3
URL imports need a filename with an extension. If the URL ends in a bare ID:
notion file import <url> --content-type image/jpeg → reuse the printed ID as the source.
Batch (minimize tool calls)
notion batch --dry-run --data '[
{"op":"get","type":"page","id":"<page_id>"},
{"op":"create","type":"page","parent":"<db_id>","data":{"title":"New"}},
{"op":"update","type":"page","id":"<page_id2>","data":{"Status":"Done"}}
]'
notion batch --llm --data '[...]' # execute
Bulk & maintenance
notion bulk update <db_id> --where "Status=Todo" --set "Status=In Progress" --dry-run
notion stats overview <db_id>
notion validate check <db_id> --check-dates --check-stale 30
notion dedup <db_id> # find duplicate pages
notion dedup <db_id> --fuzzy # include near-duplicates
notion dedup <db_id> --fix --strategy keep-largest --yes # archive duplicates
Output flags
| Flag | Use for |
|------|---------|
| --llm | Compact, structured output for agents (search, db query, find, batch, inspect schema/context, stats overview, relations backlinks) |
| --json / -j | Raw JSON for parsing |
| --csv | CSV with headers (db query, find) |
| --tsv | Tab-separated (db query, find) |
| --ids-only | One ID per line for piping (db query, search, find) |
| (default) | Human-readable |
Property type filters
--filter-prop-type is required for non-text properties:
notion db query <db_id> \
--filter-prop "Status" --filter-type equals \
--filter-value "Done" --filter-prop-type status
Types: status · select · multi_select · number · date · checkbox · people · relation
See references/filters.md for full operator reference.
Property type hints for --prop
Auto-detection treats plain strings as select. Use Key:type=Value to force a type:
notion page update <id> --prop "Status:status=Done" # status, not select
notion page update <id> --prop "Notes:rich_text=Text" # rich_text, not select
notion page update <id> --prop "Owner:people=<user_id>" # people (by id)
notion page update <id> --prop "Owner:people=ana@x.com" # people by email (member or guest)
notion page update <id> --prop "Owner:people=Ana Pérez" # people by name (case-insensitive)
For people=, a value that isn't a UUID is resolved against members (live
list) plus the guests cache: exact email first, then case-insensitive name.
Run notion user resolve-guests first so guests are resolvable. Unresolvable
values pass through unchanged.
Rules
- Property values are usually case-sensitive — verify exact status/select values with
inspect context - Property names are matched more flexibly in
0.10.0(resolvePropertyName()is case-insensitive and whitespace-tolerant), but still prefer the real schema labels for reliability - Title property name varies per DB (
"Name","Título","Task"— check state or schema) - Prefer
db query --title "..."orsearch --db <id> --exact --firstwhen you know the DB; avoid fuzzysearchfor operational updates - Use
--clear-propinstead of fake empty values likeOwner:people=orTags= --dry-runbefore any bulk/batch write- Confirm with user before destructive bulk operations
References
references/filters.md— all property types × filter operators with examplesreferences/batch-patterns.md— batch workflows (multi-update, bulk status sweep, multi-get)references/workflows.md— agent workflow recipes (task triage, weekly review, project sync)
Self-help
notion quickstart # full quick reference
notion <command> --help # per-command help
notion ai suggest <db_id> "what I want to do"
微信扫一扫