| name | notion-cli-agent |
| description | Local Notion CLI for shell-based access to pages, databases, tasks, content blocks; bulk/batch ops. Prefer over Notion MCP or direct API. |
notion-cli-agent
Local CLI for full Notion access.
Binary
notion <args>
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_TOKEN env var
NOTION_API_KEY env 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:
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:
NOTION_TOKEN=$(cat ~/.config/notion/api_key) notion <cmd>
Workspace sync (do this first)
notion sync
notion list
notion list --json
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, or notion list if already synced)
- Understand schema —
notion inspect context "Tasks" or notion inspect schema "Tasks" --llm
- Query deterministically first — prefer
db query --title, search --exact --db --first, or --llm over fuzzy workspace-wide search when you know the target DB
- Write with
--dry-run first on bulk/batch ops, then confirm with user
Core Commands
Discover
notion sync
notion list
notion inspect ws --compact
notion inspect schema "Tasks" --llm
notion inspect context "Tasks"
notion ai prompt "Tasks"
Users & guests
notion user list
notion user resolve-guests
notion user get <user_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
notion db query "Tasks" --title "Known Page" --json
notion db query "Tasks" --limit 20 --llm
notion search "keyword" --limit 10
notion search "keyword" --db <db_id> --llm
notion search "short title" --exact --first --json
notion find "overdue tasks unassigned" -d <db_id> --llm
notion find "high priority" -d <db_id> --explain
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>
notion page get <page_id> --content
notion page get <page_id> --json
notion page read <page_id>
notion page read <page_id> --blocks
notion page read <page_id> -o page.md
notion ai summarize <page_id>
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"
notion page update <page_id> --clear-prop "Tags" --clear-prop "Deadline"
notion page write <page_id> -f content.md
notion page write <page_id> -f doc.md --replace
notion page edit <page_id> --at 3 --delete 2
notion page edit <page_id> --at 5 --markdown "New text"
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"
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 '[...]'
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>
notion dedup <db_id> --fuzzy
notion dedup <db_id> --fix --strategy keep-largest --yes
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"
notion page update <id> --prop "Notes:rich_text=Text"
notion page update <id> --prop "Owner:people=<user_id>"
notion page update <id> --prop "Owner:people=ana@x.com"
notion page update <id> --prop "Owner:people=Ana Pérez"
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 "..." or search --db <id> --exact --first when you know the DB; avoid fuzzy search for operational updates
- Use
--clear-prop instead of fake empty values like Owner:people= or Tags=
--dry-run before any bulk/batch write
- Confirm with user before destructive bulk operations
References
references/filters.md — all property types × filter operators with examples
references/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
notion <command> --help
notion ai suggest <db_id> "what I want to do"