Installs into .claude/skills of the current project.
Are you the author of Notion Integration?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/builderio-notion-integration)
---
name: notion-integration
description: >-
How Notion sync works. Covers connecting, linking pages, pulling from Notion,
pushing to Notion, and checking sync status.
---
# Notion Integration
The content app can sync documents bidirectionally with Notion. Documents can be linked to Notion pages, pulled from Notion, or pushed to Notion.
Notion sync is not creative-context retrieval. When drafting new copy from a
synced page, read the `creative-context` skill first and retrieve voice,
terminology, audience guidance, and factual evidence as separate roles. Apply
its exact reuse ladder, respect opt-out/pinned packs, and use app-local Notion
content as the fallback when the shared corpus has no relevant evidence. Keep
the resulting immutable `contextPackId` and reuse labels with document
generation provenance; never infer them from a later Notion sync snapshot.
## Scripts
### connect-notion-status
Check the Notion connection status.
```bash
pnpm action connect-notion-status
```
Returns whether a Notion integration is connected and which workspace it belongs to.
### link-notion-page
Link a local document to a Notion page for syncing.
```bash
pnpm action link-notion-page --documentId abc123 --pageId <notion-page-id-or-url>
```
`--pageIdOrUrl` and `--url` are accepted aliases for `--pageId`. There is no
`--notionPageId` flag — passing it is silently dropped by the action's schema
and the action fails with "documentId and pageId are required".
### create-and-link-notion-page
Create a brand-new Notion page from a Content document's current content and
link it in one step (instead of creating in Notion first and linking after).
```bash
pnpm action create-and-link-notion-page --documentId abc123 [--parentPageIdOrUrl <id-or-url>]
```
### unlink-notion-page
Remove the sync link between a document and its Notion page without deleting
either side's content.
```bash
pnpm action unlink-notion-page --documentId abc123
```
### list-notion-links
List all documents that are linked to Notion pages.
```bash
pnpm action list-notion-links
```
### pull-notion-page
Pull content from a linked Notion page into the local document.
```bash
pnpm action pull-notion-page --documentId abc123
```
This overwrites the local document's content with the Notion page's content, converted to markdown.
### push-notion-page
Push local document content to the linked Notion page.
```bash
pnpm action push-notion-page --documentId abc123
```
This overwrites the Notion page's content with the local document's markdown, converted to Notion blocks.
### refresh-notion-sync-status
Check (and optionally auto-sync) the current sync status of a linked document.
This is what the editor UI polls every few seconds while a document is open.
```bash
pnpm action refresh-notion-sync-status --documentId abc123 [--autoSync true]
```
### resolve-notion-sync-conflict
Resolve a document whose link is in the `conflict` state (both sides changed
since the last sync) by picking a direction.
```bash
pnpm action resolve-notion-sync-conflict --documentId abc123 --direction pull|push
```
### sync-notion-comments
Sync comments bidirectionally between a document and its linked Notion page.
```bash
pnpm action sync-notion-comments --documentId abc123
```
### search-notion-pages
Search Notion pages visible to the current user's connected workspace (used to
find a page to link to).
```bash
pnpm action search-notion-pages --query "meeting notes"
```
### list-notion-database-sources
List Notion data sources visible to the current user's OAuth connection before
attaching one to a Content collection:
```bash
pnpm action list-notion-database-sources --query "projects"
```
The database-source pilot is read-only and uses the same per-user OAuth
connection as page sync. Choose a returned data-source ID, run
`suggest-source-join-key`, then attach it with
`attach-content-database-source --sourceType notion-database
--relationshipMode details`. Use `refresh-content-database-source` to pull a
new bounded snapshot. Never use a pasted token or claim Notion write-back.
### disconnect-notion
Disconnect the current user's Notion OAuth connection.
```bash
pnpm action disconnect-notion
```
## Raw Notion Provider API
Treat the Notion workflow actions above as shortcuts, not capability limits.
When the exact Notion endpoint, filter, pagination mode, or API version matters,
use `provider-api-catalog`, `provider-api-docs`, and `provider-api-request`
against the real Notion API. The provider API resolves auth from the user's
Notion OAuth connection, never from `NOTION_API_KEY`. For large scans, stage
results with `stageAs` and analyze them with `query-staged-dataset`.
## How Sync Works (Architecture)
Documents are stored as **Notion-Flavored Markdown (NFM)** — the exact format
Notion's `/pages/{id}/markdown` API emits and accepts. The storage form is
Notion's _canonical_ form, so a synced document is byte-identical on both sides.
- `shared/nfm.ts` is the single deterministic converter: `nfmToDoc` (NFM →
ProseMirror JSON) and `docToNfm` (ProseMirror JSON → NFM), plus
`canonicalizeNfm = docToNfm ∘ nfmToDoc`. It is used by **both** the editor
(`setContent(nfmToDoc(x))` / `docToNfm(editor.getJSON())`) and the server
(pull canonicalization + content hashing).
- The converter is a proven **fixpoint**: `docToNfm(nfmToDoc(x)) === x` for all
canonical NFM `x`, verified by `shared/nfm.spec.ts` (pure) and
`app/components/editor/nfm-editor.roundtrip.test.ts` (real TipTap schema).
Because our canonical form equals Notion's emission, pull→edit→push→pull
never drifts.
- Pulls also materialize accessible Notion child pages referenced by `<page>`
atoms. Each child becomes a local `documents` row with `parent_id` set to the
pulled parent and a `document_sync_links` row pointing at the child Notion
page, so the sidebar tree and page blocks can open the same local subpage.
Inaccessible child pages remain preserved as NFM page references.
- **Do not** route Notion content through `shared/notion-markdown.ts` (the old
tiptap-markdown bridge). It is retained only for clipboard copy/paste.
Supported losslessly: paragraphs, headings (incl. toggle headings via
`{toggle="true"}`), bulleted/numbered/to-do lists with tab nesting, real quote
blocks (multi-line via `<br>`), block colors (`{color="…"}`), inline
bold/italic/strike/code/underline/color/background and links, inline + block
equations, code blocks, dividers, `<empty-block/>`, callouts, toggles, columns,
tables (header row/column, cell/row colors), images/audio/video/file/pdf, page
and database references, synced blocks (children preserved), mentions, and
backslash-escaped special characters. Visual indentation is a block `indent`
attribute (Tab indents a block, matching Notion).
## Sync State
The `document_sync_links` table tracks sync relationships:
| Column | Description |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `document_id` | Local document ID |
| `provider` | Always "notion" |
| `remote_page_id` | Notion page ID |
| `state` | "linked", "syncing", "error", "conflict" |
| `last_synced_at` | Timestamp of last successful sync |
| `last_synced_content_hash` | SHA-256 of the canonical content identical on both sides — the authoritative "did it change" signal (immune to timestamp jitter) |
| `has_conflict` | Whether both sides changed since last sync (0 or 1) |
| `last_error` | Error message if sync failed |
Conflict detection is **content-hash based**: a side has "changed" only when its
canonical content hash differs from `last_synced_content_hash`. A no-op sync
(identical canonical content) is never mistaken for an edit — this is what keeps
the two copies from drifting.
## Common Tasks
| User says | What to do |
| ------------------------------ | ------------------------------------------------------ |
| "Is Notion connected?" | `connect-notion-status` |
| "Link this doc to Notion" | `link-notion-page --documentId ... --pageId ...` |
| "Pull from Notion" | `pull-notion-page --documentId ...` |
| "Push to Notion" | `push-notion-page --documentId ...` |
| "Show Notion-linked documents" | `list-notion-links` |
## Important Notes
- Notion access is **per-user OAuth only**. Never read `NOTION_API_KEY` from the
environment or `process.env`, never accept a user-pasted token or save a
user-entered Notion token through `/_agent-native/env-vars`, and require
editor access for routes that pull or push Notion content.
- Pull replaces local content with Notion's; push replaces Notion's with local.
When both sides changed since the last sync the link enters `conflict` state and
the user resolves it (pull-wins or push-wins) — there is no line-level merge.
- Because storage is canonical NFM, a no-op sync changes nothing: editing the
same document in Notion and in the app will not create growing inconsistencies.
- Always check `connect-notion-status` before attempting sync operations.