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.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add LeoYeAI/openclaw-master-skills --skill solid-notion --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Solid Notion?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/leoyeai-solid-notion)More formats (shields.io, HTML) on the badges page.
---
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):
```bash
npm install -g solid-notion
solid-notion --version
```
For local development from source:
```bash
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:
1. Visit [https://www.notion.so/profile/integrations/internal](https://www.notion.so/profile/integrations/internal)
2. Click "New integration" and give it a name
3. Copy the "Internal Integration Token"
4. Share your pages with this integration (via page "Share" settings)
### Check current auth status
```bash
solid-notion auth status --json
```
Returns:
```json
{
"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)
```bash
printf "%s" "$NOTION_TOKEN" | solid-notion init --token-stdin --json
```
Returns on success:
```json
{
"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
```bash
solid-notion auth logout --json
```
---
## 2. Command Reference
### 2.1 Browse
#### List locally pulled pages
```bash
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)
```bash
solid-notion pages
```
Output: tab-separated lines of `<last_edited>\t<page_id>\t<title>`.
#### Search pages
```bash
solid-notion search <query>
```
Output: same tab-separated format as `pages`.
### 2.2 Read
#### Show a page (non-recursive, stdout)
```bash
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)
```bash
solid-notion show block <block_id> --format json
```
#### Pull a page to local files
```bash
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
```bash
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:
```bash
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.
```json
{
"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`
```json
{
"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`
- `divider` needs only `type`
- `code` supports optional `language` (defaults to `plain text`)
```json
{
"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_3`
- `bulleted_list_item`, `numbered_list_item`, `to_do`, `quote`, `callout`
- `code`, `divider`
##### `set_props`
Updates page properties (title, rich_text, number, checkbox, select, multi_select, date).
```json
{
"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
```bash
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
```bash
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:
1. **Phase 0: Load** — Reads all edit log files from `edit-logs/<page_id>/` that don't have `.submitted` markers
2. **Phase 1: Prepare** — Fetches before-snapshots (current block content, property values) so rollback is possible
3. **Phase 2: Apply** — Applies ops sequentially to Notion
4. **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:**
```json
{
"ok": true,
"commit_id": "cmt_20260302_143022",
"notion_id": "3d1b-...",
"status": "pushed",
"applied_ops": 3,
"included_edits": 2
}
```
**Best practices:**
- Always run `submit` after `edit` operations 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
```bash
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:
```json
{
"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 as `set_props` plus `url`, `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, `props` must 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`:
1. **Phase 0: Parse** — Read and validate JSON from stdin
2. **Phase 1: Prepare** — Verify parent exists, derive internal ops, create commit draft
3. **Phase 2: Apply** — Create page on Notion (metadata only)
4. **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):**
```json
{
"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:**
```json
{
"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
```bash
solid-notion history <page_id>
```
Output: tab-separated `<id>\t<created_at>\t<type>`.
Types:
- `changeset` — Created by `write` or `restore`
- `new` — Created by `new` command
- `submit` — Created by `submit` command
#### Restore to a previous changeset or version
```bash
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 (`submit` or `new`)**: Restore to the target hash by undoing only the later `submit` versions, 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
```bash
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)
1. **Pull** the page to a workspace:
```bash
solid-notion pull page <page_id> --format markdown --outdir ~/.local/share/solid-notion-cli/<page_id>
```
2. **Edit** the local `page.md` file as needed.
3. **Write** changes back:
```bash
solid-notion write <page_id>
```
4. **Submit** with a message:
```bash
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.
1. **Get the target block IDs** (you may need to pull JSON first):
```bash
solid-notion pull page <page_id> --format json --outdir /tmp
# or inspect specific block:
solid-notion show block <block_id> --format json
```
2. **Construct a valid patch** with `ops` array and `notes`:
```json
{
"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"
}
```
3. **Apply the patch** via stdin:
```bash
cat patch.json | solid-notion edit <page_id>
```
4. **Submit** the pending edits:
```bash
solid-notion submit <page_id> -m "Applied review feedback"
```
**CRITICAL rules for JSON patch editing:**
- The patch must have exactly `ops` and `notes` — no extra keys
- Each op must have `op`, `reason`, and type-specific fields
- `block_id` in `replace_block_text` must be a valid Notion block ID
- `page_id` in `set_props` must be the target page UUID
- `append_blocks` creates new blocks — the `created_block_ids` are 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:
```bash
# 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
1. **List** changesets:
```bash
solid-notion history <page_id>
```
2. **Restore** to a specific hash:
```bash
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 successful `write`
**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 `.submitted` on successful `submit`
**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 `submit` attempt (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 `history` and `restore`
- **Format**: Markdown files with YAML frontmatter containing before/after content
- **Lifecycle**: Created on `write`, referenced by `history` and `restore`
### 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-stdin` to avoid leaking tokens in process lists.
- **Do NOT** skip `solid-notion auth status --json` before 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`, or `restore`. These commands require strict page UUIDs.
### Output parsing
- **Do NOT** forget `--json` when you need to parse command output programmatically.
### Submit command
- **Do NOT** call `submit` without `-m`. The message flag is **required**.
- **Do NOT** assume `submit` takes stdin — it reads from local `edit-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 `ops` and `notes` allowed).
- **Do NOT** forget the `reason` field in each op — it is required.
- **Do NOT** use unsupported block types in `replace_block_text` or `append_blocks`.
- **Do NOT** use `set_props` on 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**").
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!