solid-notion
Manage Notion pages locally as Markdown: pull, edit with JSON patches, write changes, submit edits with rollback, and handle API authentication via solid-not...
Description
name: solid-notion description: Read, edit, and write Notion pages as Markdown using the solid-notion CLI. Use when pulling Notion content, editing pages, managing changesets, submitting edits, restoring changes, or setting up Notion API authentication. Keywords: solid-notion, Notion, Notion API, markdown, pull, edit, write, submit, restore, changeset, notion page, notion block. metadata: author: DZ Chen version: "0.1"
solid-notion CLI Guide
solid-notion is a CLI for reading, editing, and writing Notion pages as Markdown with local reversible changesets.
0. Installation
For normal usage (published package):
npm install -g solid-notion
solid-notion --version
For local development from source:
pnpm install
pnpm build
1. Authentication Setup
Before using any command that talks to Notion, a token must be configured.
Getting a Notion Token
Create a Notion integration to get an API token:
- Visit https://www.notion.so/profile/integrations/internal
- Click "New integration" and give it a name
- Copy the "Internal Integration Token"
- Share your pages with this integration (via page "Share" settings)
Check current auth status
solid-notion auth status --json
Returns:
{
"ok": true,
"profile": "default",
"config_path": "...",
"token_present": true,
"token_fingerprint": "a1b2c3d4",
"token_valid": null
}
If token_present is false, run init first.
Save a token (agent-recommended method)
printf "%s" "$NOTION_TOKEN" | solid-notion init --token-stdin --json
Returns on success:
{
"ok": true,
"action": "init",
"profile": "default",
"config_path": "...",
"token_saved": true,
"overwritten": false,
"ignored_inputs": [],
"dry_run": false
}
Other token input methods (in precedence order):
| Method | Flag | Notes |
|---|---|---|
| Direct | --token <value> |
Visible in ps / shell history |
| Stdin | --token-stdin |
Recommended for agents |
| JSON | --input-json '{"token":"..."}' |
Useful for structured protocols |
Additional flags:
| Flag | Effect |
|---|---|
--json |
Machine-readable JSON output only |
--dry-run |
Preview without writing |
--force |
Overwrite existing token |
--profile <name> |
Use a named profile (default: "default") |
Remove a token
solid-notion auth logout --json
2. Command Reference
2.1 Browse
List locally pulled pages
solid-notion ls
solid-notion ls --json
Lists all pages that have been pulled to $SOLID_NOTION_HOME/. Does not call the Notion API.
Default output: tab-separated <pulled_at>\t<page_id>\t<title>, sorted newest first.
JSON output (--json): array of objects with page_id, title, pulled_at, path.
List all pages (remote)
solid-notion pages
Output: tab-separated lines of <last_edited>\t<page_id>\t<title>.
Search pages
solid-notion search <query>
Output: same tab-separated format as pages.
2.2 Read
Show a page (non-recursive, stdout)
solid-notion show page <page_id_or_name> --format markdown
solid-notion show page <page_id_or_name> --format json
<page_id_or_name> can be a UUID or a page title. Default format: markdown.
Show a block (JSON only)
solid-notion show block <block_id> --format json
Pull a page to local files
solid-notion pull page <page_id_or_name> --format markdown --outdir ./output
Options:
| Flag | Default | Description |
|---|---|---|
--format <format> |
json |
json or markdown |
--outdir <dir> |
$SOLID_NOTION_HOME/<page_id> |
Output directory |
--no-local-images |
(images downloaded) | Skip downloading images |
--no-local-videos |
(videos downloaded) | Skip downloading videos |
--no-recursive |
(recursive) | Only first-level blocks |
Outputs the path of the written file.
If the page was already pulled locally, running pull page again fetches the latest content from Notion and overwrites local output files in that directory.
Pull a block to local file
solid-notion pull block <block_id> --outdir ./output
Options: --outdir <dir>, --no-recursive
2.3 Edit and Write
All edit/write/submit commands require a strict Notion page ID (UUID format like aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee). Page names are NOT accepted.
Apply a JSON patch
Pipe a JSON patch object to stdin:
echo '{"ops": [...]}' | solid-notion edit <page_id>
Also accepts a markdown file path (notion-page-<uuid>.md) to resolve the page ID.
CRITICAL: Valid Patch Schema
The patch object must have exactly these keys: ops (array) and notes (string). No other keys allowed.
{
"ops": [
{ "op": "replace_block_text", "block_id": "...", "new_markdown": "...", "reason": "..." },
{ "op": "append_blocks", "parent_block_id": "...", "blocks": [...], "reason": "..." },
{ "op": "set_props", "page_id": "...", "set": {...}, "reason": "..." }
],
"notes": "optional notes"
}
Three allowed operation types:
replace_block_text
Replaces rich_text content of a block. Supported block types:
paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, to_do, quote, callout
{
"op": "replace_block_text",
"block_id": "block-uuid",
"new_markdown": "## New heading",
"reason": "Clarified section title"
}
append_blocks
Appends new blocks under a parent block.
- Most block types need
type+rich_text_md dividerneeds onlytypecodesupports optionallanguage(defaults toplain text)
{
"op": "append_blocks",
"parent_block_id": "parent-uuid",
"blocks": [
{ "type": "paragraph", "rich_text_md": "New paragraph content" },
{ "type": "heading_2", "rich_text_md": "New section" }
],
"reason": "Added conclusion section"
}
Allowed block types:
paragraph,heading_1,heading_2,heading_3bulleted_list_item,numbered_list_item,to_do,quote,calloutcode,divider
set_props
Updates page properties (title, rich_text, number, checkbox, select, multi_select, date).
{
"op": "set_props",
"page_id": "page-uuid",
"set": {
"Status": { "type": "select", "name": "Done" },
"Priority": { "type": "number", "value": 3 },
"Done": { "type": "checkbox", "value": true },
"Tags": { "type": "multi_select", "names": ["urgent", "important"] },
"Due Date": { "type": "date", "start": "2026-03-02", "end": null },
"Title": { "type": "title", "md": "New page title" },
"Notes": { "type": "rich_text", "md": "Some notes" }
},
"reason": "Updated status to Done"
}
Property value types:
| Type | Shape | Example |
|---|---|---|
number |
{ "type": "number", "value": 42 } |
- |
checkbox |
{ "type": "checkbox", "value": true } |
- |
select |
{ "type": "select", "name": "Option" } |
- |
multi_select |
{ "type": "multi_select", "names": ["A", "B"] } |
- |
date |
{ "type": "date", "start": "2026-03-02", "end": null } |
end optional |
title |
{ "type": "title", "md": "markdown" } |
- |
rich_text |
{ "type": "rich_text", "md": "markdown" } |
- |
Write workspace changes back to Notion
solid-notion write <page_id>
Reads the workspace files (page.md, .original.md), replaces the page content in Notion, creates a changeset, and cleans up the workspace.
Submit pending edits with rollback
solid-notion submit <page_id> -m "description of changes"
CRITICAL: How submit works
submit consumes pending edit logs (created by previous edit commands) and applies them to Notion with transaction-like semantics:
- Phase 0: Load — Reads all edit log files from
edit-logs/<page_id>/that don't have.submittedmarkers - Phase 1: Prepare — Fetches before-snapshots (current block content, property values) so rollback is possible
- Phase 2: Apply — Applies ops sequentially to Notion
- Phase 3: Finalize — On success, saves commit to
versions/. On failure, rolls back already-applied ops using before-snapshots.
Exit statuses:
status |
Meaning | ok |
|---|---|---|
pushed |
All ops applied successfully | true |
nothing_to_submit |
No pending edits found | false |
rolled_back |
Apply failed but rollback succeeded | false |
failed_needs_reconcile |
Apply failed AND rollback partially failed | false |
Submit result format:
{
"ok": true,
"commit_id": "cmt_20260302_143022",
"notion_id": "3d1b-...",
"status": "pushed",
"applied_ops": 3,
"included_edits": 2
}
Best practices:
- Always run
submitaftereditoperations to publish changes - If submit returns
rolled_back, the page is back to original state — no manual cleanup needed - If submit returns
failed_needs_reconcile, manual intervention may be required - The
-m(message) flag is required
2.4 Create New Pages
CRITICAL: Use the new command to create Notion pages programmatically
The new command creates a new page under a parent (page or database) with metadata only (title, icon, cover, props) via stdin as JSON. After creation, the page is automatically pulled locally as markdown. To add content (blocks), use the existing edit + submit workflow.
Create a new page
echo '{"title":"My New Page","notes":""}' | solid-notion new --parent <parent_id> -m "Create page" --json
Required flags:
--parent <parent_id>— Parent page or database ID-m, --message <message>— Commit message
Optional flags:
--database— Parent is a database (creates as database entry)--json— Output JSON only--dry-run— Validate without creating
CRITICAL: Valid Payload Schema
The JSON payload via stdin must have exactly these keys:
{
"title": "Page Title",
"icon": { "type": "emoji", "emoji": "📝" },
"cover": { "type": "external", "url": "https://..." },
"props": {
"Status": { "type": "select", "name": "In Progress" }
},
"notes": "optional notes about this creation"
}
Required fields:
title(string, non-empty)notes(string, can be empty)
Optional fields:
icon—{ "type": "emoji", "emoji": "🎉" }or{ "type": "external", "url": "..." }cover—{ "type": "external", "url": "..." }props— Page properties (database entries), same types asset_propsplusurl,email,phone_number
No blocks field. Content is added via edit + submit after creation.
Property types for props (database entries):
All types from set_props plus:
url:{ "type": "url", "value": "https://..." }email:{ "type": "email", "value": "user@example.com" }phone_number:{ "type": "phone_number", "value": "+1234567890" }
CRITICAL rules:
- Payload must have exactly
title,notes, and optional fields — no extra keys - For database pages,
propsmust match the database schema - After creation, the page is auto-pulled locally as markdown
Transaction semantics:
new follows the same 4-phase pipeline as submit:
- Phase 0: Parse — Read and validate JSON from stdin
- Phase 1: Prepare — Verify parent exists, derive internal ops, create commit draft
- Phase 2: Apply — Create page on Notion (metadata only)
- Phase 3: Finalize — Persist version record, auto-pull page. On failure, rollback (archive page).
Rollback for new:
- Archives the created page (soft-delete)
Success output (JSON mode):
{
"ok": true,
"commit_id": "parent-id-20260304T120000Z",
"action": "new",
"status": "pushed",
"created_page_id": "abc123-def456",
"page_url": "https://notion.so/abc123def456",
"title": "My New Page",
"parent_id": "parent-id",
"parent_type": "page",
"pulled_to": "/path/to/notion-page-abc123-def456.md"
}
Dry run output:
{
"ok": true,
"action": "new",
"status": "dry_run",
"dry_run": true,
"validation": "passed",
"title": "My New Page",
"parent_id": "parent-id"
}
2.5 History and Restore
View history
solid-notion history <page_id>
Output: tab-separated <id>\t<created_at>\t<type>.
Types:
changeset— Created bywriteorrestorenew— Created bynewcommandsubmit— Created bysubmitcommand
Restore to a previous changeset or version
solid-notion restore <page_id> <changeset_or_commit_id>
solid-notion restore <changeset_or_commit_id>
Behavior depends on type:
- Changeset (from
write/restore): Restores page to that changeset's state - Version (
submitornew): Restore to the target hash by undoing only the latersubmitversions, then writes a new changeset
Hash-only lookup is supported. If the hash exists in multiple pages, CLI asks you to disambiguate with page ID.
After restore-to-version, local version files after the target hash are deleted.
Outputs the new changeset ID created by restore.
3. Agent Workflows
Workflow: Read a page as Markdown
solid-notion pull page <page_id_or_name> --format markdown --outdir /tmp/notion-work
Then read the output file path printed to stdout.
Workflow: Full edit cycle (Markdown mode)
-
Pull the page to a workspace:
solid-notion pull page <page_id> --format markdown --outdir ~/.local/share/solid-notion-cli/<page_id> -
Edit the local
page.mdfile as needed. -
Write changes back:
solid-notion write <page_id> -
Submit with a message:
solid-notion submit <page_id> -m "Updated section headings"
Workflow: Edit via JSON patch (Programmatic mode)
Use this workflow when making precise, targeted edits without pulling full markdown.
-
Get the target block IDs (you may need to pull JSON first):
solid-notion pull page <page_id> --format json --outdir /tmp # or inspect specific block: solid-notion show block <block_id> --format json -
Construct a valid patch with
opsarray andnotes:{ "ops": [ { "op": "replace_block_text", "block_id": "block-uuid-here", "new_markdown": "Updated content here", "reason": "Clarified the explanation" }, { "op": "set_props", "page_id": "page-uuid-here", "set": { "Status": { "type": "select", "name": "In Progress" } }, "reason": "Moving to next phase" } ], "notes": "Batch update from review session" } -
Apply the patch via stdin:
cat patch.json | solid-notion edit <page_id> -
Submit the pending edits:
solid-notion submit <page_id> -m "Applied review feedback"
CRITICAL rules for JSON patch editing:
- The patch must have exactly
opsandnotes— no extra keys - Each op must have
op,reason, and type-specific fields block_idinreplace_block_textmust be a valid Notion block IDpage_idinset_propsmust be the target page UUIDappend_blockscreates new blocks — thecreated_block_idsare recorded in edit logs for rollback
Workflow: Batch multiple edits before submit
You can run multiple edit commands before a single submit. All pending edits are aggregated:
# First edit
echo '{"ops":[...],"notes":"First change"}' | solid-notion edit <page_id>
# Second edit
echo '{"ops":[...],"notes":"Second change"}' | solid-notion edit <page_id>
# Third edit
echo '{"ops":[...],"notes":"Third change"}' | solid-notion edit <page_id>
# Submit all at once
solid-notion submit <page_id> -m "Batch: three related updates"
Submit reads all edit logs from edit-logs/<page_id>/, marks them .submitted on success, and creates a single commit record.
Workflow: Restore a change
-
List changesets:
solid-notion history <page_id> -
Restore to a specific hash:
solid-notion restore <page_id> <changeset_or_commit_id> # or hash-only when unique: solid-notion restore <changeset_or_commit_id>
4. Storage Concepts & Layout
All data is stored under $SOLID_NOTION_HOME (default: ~/.local/share/solid-notion-cli).
Storage Concepts Explained
Workspace files (<page_id>/) — Created by pull, used by write:
page.md— The markdown you edit.original.md— Snapshot before editing (for diff/comparison)page.meta.md— Page metadata (properties, etc.)- Lifecycle: Created on
pull, deleted on successfulwrite
Edit logs (edit-logs/<page_id>/) — Created by edit, consumed by submit:
- JSONL files recording each patch operation with before/after snapshots
- Purpose: Enable rollback if submit fails
- Format: Each line is a JSON object with
op,block_id/page_id,reason, and before/after values - Lifecycle: Created on
edit, marked.submittedon successfulsubmit
Versions (<page_id>/versions/) — Created by submit:
- Commit records with full operation history and snapshots
- Purpose: Immutable history of what was published to Notion
- Format: JSON with
commitId,status,ops,beforeSnapshots,applyState - Lifecycle: Created on every
submitattempt (even failures)
Changesets (changesets/<page_id>/) — Created by write and restore:
- Reversible markdown diffs for the "write -> restore" workflow
- Purpose: Track full page content changes for
historyandrestore - Format: Markdown files with YAML frontmatter containing before/after content
- Lifecycle: Created on
write, referenced byhistoryandrestore
Directory Layout
$SOLID_NOTION_HOME/
config.json # Auth tokens (mode 0600)
# Workspace (markdown editing workflow)
<page_id>/
page.md # Edited markdown
page.meta.md # Page metadata
.original.md # Original for diff
versions/ # Submit commits (JSON)
# Edit logs (JSON patch workflow)
edit-logs/<page_id>/
<page_id>-<timestamp>.jsonl # Pending edits (before submit)
<page_id>-<timestamp>.jsonl.submitted # Marker file (after submit)
# Changesets (restorable full-page versions)
changesets/<page_id>/
<page_id>-<timestamp>.md # Changeset with YAML frontmatter
Override the base directory with SOLID_NOTION_HOME environment variable.
5. Global Flags
| Flag | Effect |
|---|---|
-v, --verbose |
Print debug logs to stderr |
-V, --version |
Print version |
6. Error Handling
init command error codes
| Exit code | Error | Meaning |
|---|---|---|
| 2 | missing_arguments |
No token provided |
| 3 | invalid_token |
Token is empty or invalid |
| 4 | token_already_exists |
Token exists, use --force |
| 5 | write_failed |
Could not write config file |
General errors
All other commands exit 1 on failure and print diagnostics to stderr including error name, message, metadata (status, code, errno, syscall, hostname), cause chain, and stack trace.
7. Anti-Patterns
Authentication
- Do NOT use
--token <value>in automated flows. Use--token-stdinto avoid leaking tokens in process lists. - Do NOT skip
solid-notion auth status --jsonbefore running commands. If there is no token, all Notion API calls will fail.
Page IDs
- Do NOT pass page names to
edit,write,submit,history, orrestore. These commands require strict page UUIDs.
Output parsing
- Do NOT forget
--jsonwhen you need to parse command output programmatically.
Submit command
- Do NOT call
submitwithout-m. The message flag is required. - Do NOT assume
submittakes stdin — it reads from localedit-logs/, not from stdin. - Do NOT ignore
status: "rolled_back"— this means Notion writes failed but were undone. - Do NOT ignore
status: "failed_needs_reconcile"— this means both apply AND rollback failed.
Edit patch schema
- Do NOT include extra keys in the patch object (only
opsandnotesallowed). - Do NOT forget the
reasonfield in each op — it is required. - Do NOT use unsupported block types in
replace_block_textorappend_blocks. - Do NOT use
set_propson properties with unsupported types (only: number, checkbox, select, multi_select, date, title, rich_text). - Do NOT pass Notion block objects as
new_markdown— pass simple Markdown text (e.g., "## Heading" or "Paragraph with bold").
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!