| 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.
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.
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).
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.
pnpm action unlink-notion-page --documentId abc123
list-notion-links
List all documents that are linked to Notion pages.
pnpm action list-notion-links
pull-notion-page
Pull content from a linked Notion page into the local document.
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.
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.
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.
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.
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).
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 database:
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.
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.