Skip to content
Back to skills

Sync Notion Pages

ASecurity

Bidirectional sync between Obsidian notes and Notion pages for collaborative planning.

  • 52 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 9, 2026
businessjavascriptjavadockerapi

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add DavidROliverBA/ArchitectKB --skill sync-notion-pages --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sync Notion Pages?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Sync Notion Pages
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/davidroliverba-sync-notion-pages/badge)](https://www.skillsdirectory.com/skills/davidroliverba-sync-notion-pages)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
context: fork
---

# /sync-notion-pages

Bidirectional sync between Obsidian notes and Notion pages for collaborative planning.

## Usage

```
/sync-notion-pages                      # Check all tracked pages for changes
/sync-notion-pages <note>               # Sync a specific note
/sync-notion-pages --push <note>        # Force push to Notion
/sync-notion-pages --pull <note>        # Force pull from Notion
/sync-notion-pages --link <note> <url>  # Link note to Notion page
/sync-notion-pages --unlink <note>      # Remove sync tracking from note
/sync-notion-pages --status             # Show sync status dashboard
```

## Prerequisites

- Notion MCP server must be connected (tools like `mcp__MCP_DOCKER__API-retrieve-a-page`)
- Notes must have `notionPageId` in frontmatter to be tracked

## Instructions

### Phase 1: Parse Command

1. Identify operation mode:
   - No args: Check all tracked pages
   - `<note>`: Sync specific note (detect direction automatically)
   - `--push <note>`: Force push local → Notion
   - `--pull <note>`: Force pull Notion → local
   - `--link <note> <url>`: Link existing note to Notion page
   - `--unlink <note>`: Remove sync tracking
   - `--status`: Show dashboard

2. If note specified, find it:
   - Search by filename or title
   - Verify note exists
   - Check if tracked (has `notionPageId`)

### Phase 2: Load Manifest

Read `.claude/sync/notion-pages-manifest.json`:

```json
{
  "version": "1.0",
  "lastFullSync": "2026-01-20T14:00:00Z",
  "trackedPages": {
    "<pageId>": {
      "localFile": "Trip - Lisbon March 2026.md",
      "notionUrl": "https://...",
      "notionLastEdited": "2026-01-20T14:30:00Z",
      "localLastModified": "2026-01-20T15:00:00Z",
      "lastSynced": "2026-01-20T14:00:00Z",
      "syncStatus": "local-ahead"
    }
  }
}
```

### Phase 3: Operations

#### 3A: Link Operation (`--link`)

1. Parse Notion URL to extract page ID:
   - Format: `https://www.notion.so/Page-Title-<pageId>` or `https://www.notion.so/<pageId>`
   - Page ID is typically 32 characters with optional hyphens

2. Fetch page from Notion using `mcp__MCP_DOCKER__API-retrieve-a-page`:

   ```
   page_id: "<extracted-page-id>"
   ```

3. Add sync fields to note frontmatter:

   ```yaml
   notionPageId: "<pageId>"
   notionUrl: "<full-url>"
   lastSynced: null
   syncStatus: unsynced
   ```

4. Add entry to manifest

5. Report success

#### 3B: Status Operation (`--status`)

1. For each tracked page in manifest:
   - Fetch Notion page `last_edited_time`
   - Get local file modified timestamp
   - Calculate sync status

2. Display dashboard:

   ```markdown
   # Notion Page Sync Status

   | Note          | Status      | Local Modified   | Notion Modified  | Last Synced      |
   | ------------- | ----------- | ---------------- | ---------------- | ---------------- |
   | Trip - Lisbon | local-ahead | 2026-01-21 10:00 | 2026-01-20 14:30 | 2026-01-20 14:00 |
   ```

3. Summarise:
   - X pages synced
   - X pages local-ahead (need push)
   - X pages remote-ahead (need pull)
   - X pages in conflict

#### 3C: Check/Sync Operation (default or specific note)

For each tracked page (or specific note):

1. **Fetch Notion metadata:**

   ```
   mcp__MCP_DOCKER__API-retrieve-a-page with page_id
   ```

   Extract `last_edited_time`

2. **Get local file modified time:**
   - Read file and check `modified` in frontmatter
   - Also check filesystem modified time as backup

3. **Compare against lastSynced:**

   ```
   localChanged = localModified > lastSynced
   remoteChanged = notionLastEdited > lastSynced

   Status:
   - synced: !localChanged && !remoteChanged
   - local-ahead: localChanged && !remoteChanged
   - remote-ahead: !localChanged && remoteChanged
   - conflict: localChanged && remoteChanged
   ```

4. **Take action based on status:**
   - `synced`: Report "No changes"
   - `local-ahead`: Auto-push to Notion
   - `remote-ahead`: Auto-pull from Notion
   - `conflict`: Prompt user for resolution

#### 3D: Push Operation (`--push` or auto-push)

1. **Read local note content:**
   - Parse frontmatter
   - Extract markdown body (skip frontmatter and callouts)

2. **Convert Markdown → Notion blocks:**

   | Markdown     | Notion Block Type      |
   | ------------ | ---------------------- | --- | ----- |
   | `# H1`       | heading_1              |
   | `## H2`      | heading_2              |
   | `### H3`     | heading_3              |
   | `paragraph`  | paragraph              |
   | `- item`     | bulleted_list_item     |
   | `1. item`    | numbered_list_item     |
   | `- [ ] task` | to_do (checked: false) |
   | `- [x] task` | to_do (checked: true)  |
   | `> quote`    | quote                  |
   | `code`       | code                   |
   | `            | table                  | `   | table |

3. **Clear existing Notion blocks:**
   - Get current blocks: `mcp__MCP_DOCKER__API-get-block-children`
   - Delete each block: `mcp__MCP_DOCKER__API-delete-a-block`

4. **Append new blocks:**
   - Use `mcp__MCP_DOCKER__API-patch-block-children` to add converted blocks

5. **Update timestamps:**
   - Update frontmatter `lastSynced` to now
   - Update manifest with new sync time
   - Set `syncStatus: synced`

#### 3E: Pull Operation (`--pull` or auto-pull)

1. **Fetch Notion page content:**

   ```
   mcp__MCP_DOCKER__API-retrieve-a-page with page_id
   mcp__MCP_DOCKER__API-get-block-children with block_id=page_id
   ```

2. **Convert Notion blocks → Markdown:**

   For each block, convert to markdown:

   ```javascript
   function blockToMarkdown(block) {
     switch (block.type) {
       case "heading_1":
         return `# ${getRichText(block.heading_1.rich_text)}`;
       case "heading_2":
         return `## ${getRichText(block.heading_2.rich_text)}`;
       case "heading_3":
         return `### ${getRichText(block.heading_3.rich_text)}`;
       case "paragraph":
         return getRichText(block.paragraph.rich_text);
       case "bulleted_list_item":
         return `- ${getRichText(block.bulleted_list_item.rich_text)}`;
       case "numbered_list_item":
         return `1. ${getRichText(block.numbered_list_item.rich_text)}`;
       case "to_do":
         const checked = block.to_do.checked ? "x" : " ";
         return `- [${checked}] ${getRichText(block.to_do.rich_text)}`;
       case "quote":
         return `> ${getRichText(block.quote.rich_text)}`;
       case "code":
         return `\`\`\`${block.code.language}\n${getRichText(block.code.rich_text)}\n\`\`\``;
       case "divider":
         return "---";
       default:
         return "";
     }
   }

   function getRichText(richTextArray) {
     return richTextArray.map((t) => t.plain_text).join("");
   }
   ```

3. **Preserve frontmatter:**
   - Read current note frontmatter
   - Update `modified` to today
   - Update `lastSynced` to now
   - Set `syncStatus: synced`

4. **Preserve local-only sections:**
   - Keep callout blocks (e.g., `> [!info]`)
   - Keep frontmatter
   - Replace body content

5. **Write updated note**

6. **Update manifest**

### Phase 4: Conflict Resolution

When both sides changed since last sync:

1. **Show diff summary:**

   ```markdown
   ## Conflict Detected: Trip - Lisbon March 2026

   **Local changes since 2026-01-20 14:00:**

   - Modified flights section
   - Added new activities

   **Notion changes since 2026-01-20 14:00:**

   - Collaborator added restaurant suggestions
   - Updated accommodation notes
   ```

2. **Offer resolution options using AskUserQuestion:**
   - Keep local (push to Notion)
   - Keep remote (pull from Notion)
   - Skip (resolve manually later)

3. **Execute chosen resolution:**
   - If keep local: Run push operation
   - If keep remote: Run pull operation
   - If skip: Leave status as `conflict`

### Phase 5: Report

```markdown
# Notion Page Sync Report

**Synced at:** 2026-01-21T10:30:00Z

## Summary

| Action              | Count |
| ------------------- | ----- |
| Synced (no changes) | 2     |
| Pushed to Notion    | 1     |
| Pulled from Notion  | 0     |
| Conflicts           | 0     |
| Errors              | 0     |

## Details

### Pushed: Trip - Lisbon March 2026

- Local modified: 2026-01-21 10:00
- Notion updated: 2026-01-21 10:30

## Next Steps

- Review synced content in Notion
- Check for any formatting issues
```

## Content Preservation Rules

### Always Preserve in Local Note:

- Frontmatter (sync adds/updates specific fields)
- Local callout blocks (`> [!info]`, `> [!warning]`)
- Links to other Obsidian notes (`[[Note Name]]`)

### Always Push to Notion:

- Headings
- Paragraphs
- Lists (bulleted, numbered, tasks)
- Tables
- Code blocks

### Handle Specially:

- Wiki-links: Convert `[[Note]]` to plain text when pushing
- Callouts: Skip when pushing (Notion doesn't support same format)
- Embedded content: Skip (handle manually)

## Error Handling

| Error                   | Action                              |
| ----------------------- | ----------------------------------- |
| Notion API rate limited | Wait and retry                      |
| Page not found          | Remove from tracking, notify user   |
| Invalid page ID         | Report error, suggest re-linking    |
| Network timeout         | Retry once, then report             |
| Malformed blocks        | Skip block, continue, report at end |

## Manifest Location

`.claude/sync/notion-pages-manifest.json`

This is separate from the Confluence sync manifest at `.claude/sync/manifest.json`.

## Example Workflow

### First-time setup for a note:

```
User: /sync-notion-pages --link "Trip - Lisbon March 2026" https://www.notion.so/Lisbon-March-2026-2ee76da238c981459c88ca451e112c39

Claude:
1. Extracts page ID: 2ee76da238c981459c88ca451e112c39
2. Fetches Notion page to verify
3. Updates note frontmatter with sync fields
4. Adds to manifest
5. Reports: "Linked Trip - Lisbon March 2026 to Notion page"
```

### Regular sync check:

```
User: /sync-notion-pages

Claude:
1. Loads manifest
2. For each tracked page, checks timestamps
3. Reports status for all pages
4. Offers to sync any that need it
```

### Force push after local edits:

```
User: /sync-notion-pages --push "Trip - Lisbon March 2026"

Claude:
1. Reads local note
2. Converts to Notion blocks
3. Updates Notion page
4. Updates timestamps
5. Reports success
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…