Skip to main content

notion-integration

How Notion sync works. Covers connecting, linking pages, pulling from Notion, pushing to Notion, and checking sync status.

Source facts

Repository
BuilderIO/agent-native
Last source activity
October 10, 2026 at 04:08
Detected SKILL.md language
English
Stars
7,449
Forks
681

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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. - A Notion export is a one-time copy, not a sync. `import-content` takes the export's `.md` files and images but not the zip itself yet; link and pull pages that should stay in sync with Notion.
View on GitHub