Manage Notion notes, pages, and data sources with rich markdown support. JSON-first CLI for search, read/export, write/import, append, update, delete, and move operations. Full inline formatting, native tables, callouts, toggles, nested lists, and images.
🎉 Major improvements:
**bold**, *italic*, ~~strikethrough~~, `code`, [links](url) all work| Header | ...) → Notion table blocks with proper structure> [!NOTE], > [!TIP], > [!WARNING]) → colored Notion callouts with icons<details> tags → Notion toggle blocks → Notion image blocksget-blocks — List all blocks in a page (useful for editing workflows)update-page — Update page title and propertiesupdate-block — Update an existing block's contentdelete-block — Delete a block by ID<details> tagsPrefer deterministic scripts over ad‑hoc API calls:
This skill ships a single entrypoint CLI: {baseDir}/scripts/notionctl.mjs
Notion-Version: 2025-09-03 for every requestRetry-Afterdata_source_id, not database_idThis skill expects NOTION_API_KEY to be present in the environment.
If you need a fallback for local dev, the CLI also checks:
NOTION_TOKEN, NOTION_API_TOKEN~/.config/notion/api_keynode {baseDir}/scripts/notionctl.mjs whoami
Search pages (title match):
node {baseDir}/scripts/notionctl.mjs search --query "meeting notes" --type page
Search data sources (databases):
node {baseDir}/scripts/notionctl.mjs search --query "Inbox" --type data_source
node {baseDir}/scripts/notionctl.mjs export-md --page "<page-id-or-url>"
Export to stdout (no JSON wrapper):
node {baseDir}/scripts/notionctl.mjs export-md --page "<page-id-or-url>" --stdout-md
Under a parent page:
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<page-id-or-url>" \
--title "Meeting Notes" \
--md-file notes.md
Under a data source (database row):
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-data-source "<data-source-id-or-url>" \
--title "Task: Fix bug" \
--md "## Description\n\nFix the login timeout issue" \
--set "Status=In Progress" \
--set "Priority=High"
node {baseDir}/scripts/notionctl.mjs append-md \
--page "<page-id-or-url>" \
--md "## Update\n\n- Added feature X\n- Fixed bug Y"
Update title only:
node {baseDir}/scripts/notionctl.mjs update-page \
--page "<page-id-or-url>" \
--title "New Title"
Update database page properties:
node {baseDir}/scripts/notionctl.mjs update-page \
--page "<page-id-or-url>" \
--set "Status=Done" \
--set "Tags=urgent,reviewed"
# Get block ID first
node {baseDir}/scripts/notionctl.mjs get-blocks --page "<page-id>" | jq '.blocks[] | select(.type=="heading_2") | .id'
# Update the block
node {baseDir}/scripts/notionctl.mjs update-block \
--block "<block-id>" \
--md "## Updated Heading with **bold**"
node {baseDir}/scripts/notionctl.mjs delete-block --block "<block-id>"
Move under another page:
node {baseDir}/scripts/notionctl.mjs move \
--page "<page-id-or-url>" \
--to-page "<parent-page-id-or-url>"
Move into a database (data source):
node {baseDir}/scripts/notionctl.mjs move \
--page "<page-id-or-url>" \
--to-data-source "<data-source-id-or-url>"
| Feature | Status | Notes |
|---------|--------|-------|
| Tables | ✅ | Native Notion blocks |
| Callouts | ✅ | With colors & icons |
| Toggles | ✅ | Collapsible content |
→ Creates a proper Notion table with header row
> [!NOTE]
> This is an informational callout (blue background, 📝 icon)
> [!TIP]
> This is a helpful tip (green background, 💡 icon)
> [!WARNING]
> This is a warning (yellow/orange background, ⚠️ icon)
> [!IMPORTANT]
> This is critical info (red background, ❗ icon)
→ Creates colored Notion callout blocks with appropriate emojis
<details>
<summary>Click to expand</summary>
Hidden content here!
- Can contain lists
- And other **formatted** content
</details>
→ Creates a Notion toggle block
- Level 1
- Level 2
- Level 3
- Even deeper!
- Back to level 2
- Another level 1
1. Numbered parent
- Nested bullet
- Deeply nested
- Back to nested
2. Second numbered
→ Preserves full nesting structure in Notion
This has **bold**, *italic*, ~~strikethrough~~, `inline code`, and [a link](https://example.com).
You can combine: **bold with *italic inside* and even `code`**!
→ All annotations preserved in Notion rich text
- [x] Completed task
- [x] Completed subtask
- [ ] Incomplete subtask
- [ ] Incomplete parent
- [ ] Nested incomplete
→ Notion to-do blocks with proper nesting and checked state
# Meeting Notes - 2026-02-03
> [!TIP]
> Key action items highlighted below
## Decisions
| Decision | Owner | Deadline |
|----------|-------|----------|
| Migrate to v2 | Alex | Feb 10 |
| Add tests | Team | Feb 15 |
## Action Items
- [x] Update documentation
- [x] Add examples
- [ ] Add video tutorial
- [ ] Deploy to production
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<inbox-page-id>" \
--title "Meeting Notes - 2026-02-03" \
--md-file notes.md
node {baseDir}/scripts/notionctl.mjs export-md --page "<page-id>" --stdout-md > current.md
Edit current.md locally
Append the updates:
node {baseDir}/scripts/notionctl.mjs append-md --page "<page-id>" --md-file updates.md
Or replace a specific block:
# Get block ID of the section to update
node {baseDir}/scripts/notionctl.mjs get-blocks --page "<page-id>" | jq '.blocks[] | select(.type=="heading_2") | {id, text}'
# Update that block
node {baseDir}/scripts/notionctl.mjs update-block --block "<block-id>" --md "## Updated Section"
If your inbox is a page with child pages:
node {baseDir}/scripts/notionctl.mjs list-child-pages --page "<inbox-page-id-or-url>"
node {baseDir}/scripts/notionctl.mjs triage \
--inbox-page "<inbox-page-id>" \
--rules "{baseDir}/assets/triage-rules.example.json"
node {baseDir}/scripts/notionctl.mjs triage \
--inbox-page "<inbox-page-id>" \
--rules "{baseDir}/assets/triage-rules.example.json" \
--apply
export-md to read contentappend-md / create-md / update-block to write back--apply, cap scope with --limit, then apply--compact for single-line JSON output (easier for parsing)When creating or updating pages in a database, use --set:
# Create database page with properties
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-data-source "<db-id>" \
--title "Bug Report" \
--md "## Description\n\nLogin timeout" \
--set "Status=Open" \
--set "Priority=High" \
--set "Tags=bug,urgent" \
--set "Due=2026-02-10"
# Update existing page properties
node {baseDir}/scripts/notionctl.mjs update-page \
--page "<page-id>" \
--set "Status=Done" \
--set "Tags=completed"
Supported property types:
title, rich_text → textselect → single valuemulti_select → comma-separated valuesstatus → status namedate → ISO date string or {start: "...", end: "..."}checkbox → true/falsenumber → numeric valueurl, email, phone_number → stringspeople, relation → comma-separated IDsCreate page with a template:
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<page-id>" \
--title "Weekly Report" \
--template default
Use a specific template:
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<page-id>" \
--title "Project Plan" \
--template-id "<template-page-id>"
Insert at specific position:
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<page-id>" \
--title "First Item" \
--position page_start \
--md "Content here"
Insert after a specific block:
node {baseDir}/scripts/notionctl.mjs create-md \
--parent-page "<page-id>" \
--title "After Block X" \
--after-block "<block-id>" \
--md "Content here"
Retry-After; reduce concurrency (tool handles this automatically)|---|---|) is present> [!TYPE] on its own line (supported types: NOTE, TIP, WARNING, IMPORTANT, CAUTION)**bold *italic code***) may not parse perfectly# Info
notionctl whoami
notionctl search --query "text" [--type page|data_source|all] [--limit 20]
# Read
notionctl get-page --page "<id-or-url>"
notionctl get-blocks --page "<id-or-url>"
notionctl export-md --page "<id-or-url>" [--stdout-md]
# Create
notionctl create-md --title "Title" (--parent-page|--parent-data-source "<id>") (--md|--md-file|--md-stdin) [options]
# Update
notionctl update-page --page "<id>" [--title "..."] [--set "Prop=Value" ...]
notionctl update-block --block "<id>" (--md|--md-file|--md-stdin)
notionctl append-md --page "<id>" (--md|--md-file|--md-stdin)
# Delete
notionctl delete-block --block "<id>"
# Move
notionctl move --page "<id>" (--to-page|--to-data-source "<id>")
# Utility
notionctl list-child-pages --page "<id>"
notionctl triage --inbox-page "<id>" --rules "<json-file>" [--limit 50] [--apply]
# Global flags
--compact # Single-line JSON output
--help # Show help
get-blocks, update-page, update-block, delete-block