/sync-notion-pages
SkillDocs & knowledgeBidirectional sync between Obsidian notes and Notion pages for collaborative planning.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the /sync-notion-pages skill
About this capability
A comprehensive knowledge management system for Solutions Architects using AI
What this skill tells your AI
The instructions your AI receives, as published by davidroliverba/architectkb in .claude/skills/sync-notion-pages/SKILL.md and read by ahel’s review.
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
notionPageIdin frontmatter to be tracked
Instructions
Phase 1: Parse Command
-
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
-
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:
{
"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)
-
Parse Notion URL to extract page ID:
- Format:
https://www.notion.so/Page-Title-<pageId>orhttps://www.notion.so/<pageId> - Page ID is typically 32 characters with optional hyphens
- Format:
-
Fetch page from Notion using
mcp__MCP_DOCKER__API-retrieve-a-page:page_id: "<extracted-page-id>" -
Add sync fields to note frontmatter:
notionPageId: "<pageId>" notionUrl: "<full-url>" lastSynced: null syncStatus: unsynced -
Add entry to manifest
-
Report success
3B: Status Operation (--status)
-
For each tracked page in manifest:
- Fetch Notion page
last_edited_time - Get local file modified timestamp
- Calculate sync status
- Fetch Notion page
-
Display dashboard:
# 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 | -
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):
-
Fetch Notion metadata:
mcp__MCP_DOCKER__API-retrieve-a-page with page_idExtract
last_edited_time -
Get local file modified time:
- Read file and check
modifiedin frontmatter - Also check filesystem modified time as backup
- Read file and check
-
Compare against lastSynced:
localChanged = localModified > lastSynced remoteChanged = notionLastEdited > lastSynced Status: - synced: !localChanged && !remoteChanged - local-ahead: localChanged && !remoteChanged - remote-ahead: !localChanged && remoteChanged - conflict: localChanged && remoteChanged -
Take action based on status:
synced: Report "No changes"local-ahead: Auto-push to Notionremote-ahead: Auto-pull from Notionconflict: Prompt user for resolution
3D: Push Operation (--push or auto-push)
-
Read local note content:
- Parse frontmatter
- Extract markdown body (skip frontmatter and callouts)
-
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 | -
Clear existing Notion blocks:
- Get current blocks:
mcp__MCP_DOCKER__API-get-block-children - Delete each block:
mcp__MCP_DOCKER__API-delete-a-block
- Get current blocks:
-
Append new blocks:
- Use
mcp__MCP_DOCKER__API-patch-block-childrento add converted blocks
- Use
-
Update timestamps:
- Update frontmatter
lastSyncedto now - Update manifest with new sync time
- Set
syncStatus: synced
- Update frontmatter
3E: Pull Operation (--pull or auto-pull)
-
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 -
Convert Notion blocks → Markdown:
For each block, convert to markdown:
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(""); } -
Preserve frontmatter:
- Read current note frontmatter
- Update
modifiedto today - Update
lastSyncedto now - Set
syncStatus: synced
-
Preserve local-only sections:
- Keep callout blocks (e.g.,
> [!info]) - Keep frontmatter
- Replace body content
- Keep callout blocks (e.g.,
-
Write updated note
-
Update manifest
Phase 4: Conflict Resolution
When both sides changed since last sync:
-
Show diff summary:
## 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 -
Offer resolution options using AskUserQuestion:
- Keep local (push to Notion)
- Keep remote (pull from Notion)
- Skip (resolve manually later)
-
Execute chosen resolution:
- If keep local: Run push operation
- If keep remote: Run pull operation
- If skip: Leave status as
conflict
Phase 5: Report
# 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
Signals
- GitHub stars
- 52
- Forks
- 12
- Last commit
- Mar 2026
ahel review
S4info
community integration — published by davidroliverba, not notion
Automated review, not a security audit. Ruleset v1.
Advanced
- Catalog kind
- skill
- Gateway key
sync-notion-pages- Source
- github.com/davidroliverba/architectkb