# Notion Knowledge Sync

# Notion Knowledge Sync

The **Notion Knowledge Sync** mirrors your Amdahl knowledge base into a Notion
database **in your own Notion workspace** — a one-way export so your team can read
and link to your KB documents from inside Notion. It is the only **outbound**
connector: every other connector pulls a provider's data _into_ Amdahl (see
[Connecting data sources](connecting-data-sources.md)); this one pushes Amdahl's
knowledge base _out_ to Notion.

What gets mirrored is the **current (promoted) version** of each document — the
same version your workspace and agents read. Proposed-but-not-yet-promoted
versions are not exported until a human promotes them, so what lands in Notion is
your canonical reference library, not work-in-progress drafts.

This guide covers what the sync does, how to connect and configure it, what gets
synced and when, the REST API, the MCP read resources, and the limits to know
about.

This connector is in the
[connector catalog reference](../api-reference/connector-catalog.md) as
`notion_knowledge_sync` (Docs category, OAuth, workspace-owned, single-instance).

## What it does

- **One-way mirror.** Amdahl knowledge-base documents are written into a Notion
  database that the sync creates in your Notion workspace. Editing a document in
  Amdahl re-pushes it; the sync never reads Notion data back into Amdahl.
- **Current version only (by default).** Each Amdahl document is a version family;
  the sync mirrors the **current** (promoted) version. When you promote a new
  version in Amdahl, the Notion page updates to match.
- **One Notion page per document.** Each KB version family maps to exactly one
  Notion page (kept stable across re-syncs, so links and bookmarks survive). The
  database row carries the document title, an `Amdahl Doc ID`, the version number,
  and a last-synced timestamp.
- **Self-healing.** If you delete the mirrored page in Notion, the next sync
  recreates it. An hourly reconcile sweep backfills anything that was missed and
  brings drifted pages back in line.

The sync runs entirely inside Amdahl (there is no Notion-side puller to install)
and writes to Notion through Notion's API on your behalf using the access you
grant during connect.

## How to connect

Connecting is an **OAuth** flow. In the common case that is the _only_ step —
authorize, pick **one** page during Notion's consent, and the sync auto-creates its
database under that page and backfills. A **configure** step is only needed when you
grant access to more than one page (so you can pick which one) or want to change the
defaults later.

### 1. Authorize Notion (OAuth)

Connect `notion_knowledge_sync` the same way as any OAuth connector — from the
console Connections page, or via the REST API:

```
POST /connections
{ "connector_type": "notion_knowledge_sync", "name": "Acme KB → Notion" }
```

The response is `{ "mode": "oauth_redirect", "authorize_url": "...", "connection": { ... } }`.
Send the user to `authorize_url`; Notion asks them to approve the integration and
**select which pages it can access**. The callback stores the access token.

> Notion uses **page-scoped consent**: the pages you pick during authorize are the
> pages the sync can write to. Grant access to the page you want the synced database
> created under (or a parent of it) — Notion only lets an integration write where it
> has been given access.

### 2. Auto-provision (the one-click path)

Right after the callback, the sync looks at the pages you granted:

- **You granted exactly one page.** The sync auto-creates the "Amdahl Knowledge
  Base" database **under that page** (with the `Name`, `Amdahl Doc ID`, `Version`,
  and `Last Synced` properties it populates) and kicks off a full backfill. **No
  configure step is needed** — you're done.
- **You granted more than one page.** The sync can't guess which one to use, so it
  waits for you to pick. The connection's status reports
  `"provisioning_state": "needs_parent_selection"`; show the picker (below) and then
  call `configure` with the chosen page.
- **You granted no usable pages.** The status reports
  `"provisioning_state": "no_pages"` — re-authorize and grant a page.

Poll `GET /notion-sync/:id/status` after connecting; its `provisioning_state` tells
you which case you are in.

### 3. Pick a parent page (only when you granted more than one)

List the pages the integration can access and let the user choose:

```
GET /notion-sync/:id/pages
```

```json
{
  "pages": [
    { "id": "<page-id>", "title": "Engineering Wiki", "url": "https://notion.so/..." },
    { "id": "<page-id>", "title": "Sales Playbook", "url": "https://notion.so/..." }
  ]
}
```

Then call `configure` with the chosen page id (see below).

### 4. Configure (pick a parent page + provision the database)

`configure` is the manual path — used to pick a parent page in the multi-page case,
or to change the defaults / re-point the sync later. It provisions the database
under the designated page and kicks a backfill:

```
POST /notion-sync/:id/configure
{
  "parent_page_id": "<notion-page-id-or-url>",
  "database_title": "Amdahl Knowledge Base",
  "version_policy": "current_only",
  "on_kb_delete": "archive",
  "drift_policy": "preserve",
  "include": { "starred_only": false }
}
```

`parent_page_id` accepts either a bare Notion page id or a full Notion page URL.
Only `parent_page_id` is required; everything else takes a sensible default
(see [Configure](#configure-options) below). The response is
`{ "connection_id": "...", "config": { ... }, "backfill_enqueued": <n> }`, where
`backfill_enqueued` is how many documents were queued for the initial mirror.

Re-running `configure` re-provisions a fresh database and re-baselines — use it
to point the sync at a different parent page.

## Configure options

The configure call (and the console settings panel) accept these knobs. They are
stored on the connection and applied to every sync.

| Option           | Values                          | Default          | What it controls                                                                                                                                        |
| ---------------- | ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `parent_page_id` | a Notion page id                | _(required)_     | The Notion page the synced database is created under.                                                                                                   |
| `version_policy` | `current_only` / `all_promoted` | `current_only`   | Which versions to mirror. `current_only` mirrors just the promoted version (respects the human promotion gate).                                         |
| `on_kb_delete`   | `archive` / `leave`             | `archive`        | What happens to the Notion page when the source document is archived or deleted in Amdahl. `archive` trashes the page; `leave` keeps it.                |
| `drift_policy`   | `preserve` / `overwrite`        | `overwrite`      | What happens when a human has edited the Notion page since the last sync. `preserve` keeps the human edit and skips; `overwrite` re-writes from Amdahl. |
| `include`        | scope filters (below)           | all current docs | Which documents are in scope.                                                                                                                           |
| `enabled`        | `true` / `false`                | `true`           | Master on/off for this connection's sync.                                                                                                               |

**Include filters** (`include`) narrow which documents mirror. Leave empty to
mirror every current document:

- `document_types`: only documents whose KB `document_type` is in this list.
- `starred_only`: only starred documents.
- `group_ids`: an explicit allowlist of document version-family ids.

## What gets synced, and when

The sync fires on two paths.

**Instantly, off your KB writes.** When you upload or append to a knowledge-base
document, or promote a version to current, the change is queued for Notion right
away (off the write path, so it never slows the KB write). Rapid-fire edits to the
same document collapse into one sync that always reflects the latest version.

**Hourly, via the reconcile sweep.** An hourly background sweep compares your
current KB documents against what is mirrored and queues anything that is out of
sync — unmapped (never synced), version-drifted, errored, or needing archival.
This is what backfills an entire workspace on first connect and what self-heals
anything the instant path missed (a transient Notion error, an exhausted retry, a
page you deleted in Notion).

**Removals.** When a document is archived or hard-deleted in Amdahl, its Notion
page is trashed on the next sync if `on_kb_delete` is `archive` (the default), or
left in place if `leave`. Removals propagate either through the KB write hooks or
the hourly reconcile sweep.

A document whose content has not changed since the last push is skipped without a
Notion round-trip (a content hash is compared), so backfills and reconcile sweeps
are cheap and safe to repeat.

## The REST API

The Notion-sync REST surface lives under the platform base URL
(`https://api.amdahl.com/api/platform/v1`) and authenticates with a bearer token.
See [Authentication](../authentication.md). `:id` is the Notion-sync connection id
from `POST /connections` (or `GET /connections`).

### Read the config

```
GET /notion-sync/:id
```

Returns the sync configuration plus how many documents are currently mirrored:

```json
{
  "connection_id": "11111111-1111-1111-1111-111111111111",
  "name": "Acme KB → Notion",
  "status": "connected",
  "config": {
    "enabled": true,
    "target_mode": "database",
    "parent_page_id": "<page-id>",
    "database_id": "<database-id>",
    "data_source_id": "<data-source-id>",
    "version_policy": "current_only",
    "on_kb_delete": "archive",
    "drift_policy": "overwrite",
    "include": {},
    "provisioned_at": "2026-06-27T12:00:00.000Z"
  },
  "mapped_count": 42
}
```

A non-Notion-sync or cross-tenant id returns `null` — a not-found that never
reveals whether the id exists in another workspace.

### Read the live status (for a polling badge)

```
GET /notion-sync/:id/status
```

A lean projection for polling a status badge or confirming a backfill is
progressing:

```json
{
  "connection_id": "11111111-1111-1111-1111-111111111111",
  "status": "connected",
  "enabled": true,
  "provisioned": true,
  "provisioning_state": "provisioned",
  "database_id": "<database-id>",
  "mapped_count": 42,
  "recent": { "syncedLastHour": 7, "failedLastHour": 0, "skippedLastHour": 3 }
}
```

`provisioning_state` reports where the connect-only auto-provision flow left off:
`provisioned` (the database was auto-created), `needs_parent_selection` (you granted
more than one page — show the picker and call `configure`), `no_pages` (no page was
granted), or `none` (auto-provision has not run).

### List the integration's accessible pages (the picker)

```
GET /notion-sync/:id/pages
```

Lists the Notion pages the connected integration can access — the parent-page
picker for when a Notion authorize granted more than one page. Returns
`{ "pages": [ { "id", "title", "url" }, ... ] }`. A non-Notion-sync or cross-tenant
id returns an empty list.

### Read the activity ledger

```
GET /notion-sync/:id/sends
```

Outbound connectors have no upstream sync-run history, so the per-document sync
ledger is the activity log. Optional `?limit=` (1–200, default 50). The response
is the recent outcomes (newest-first) plus an hourly summary:

```json
{
  "rows": [
    {
      "id": "...",
      "documentGroupId": "<doc-group-id>",
      "notionPageId": "<notion-page-id>",
      "event": "promote",
      "status": "synced",
      "skipReason": null,
      "error": null,
      "createdAt": "2026-06-27T12:01:00.000Z"
    },
    {
      "id": "...",
      "documentGroupId": "<doc-group-id>",
      "notionPageId": null,
      "event": "upload",
      "status": "skipped",
      "skipReason": "unchanged_hash",
      "error": null,
      "createdAt": "2026-06-27T11:55:00.000Z"
    }
  ],
  "summary": { "syncedLastHour": 7, "failedLastHour": 0, "skippedLastHour": 3 }
}
```

Each row's `event` is what triggered the attempt (`upload`, `promote`, `archive`,
`delete`, `backfill`, `reconcile`); `status` is `synced` / `skipped` / `failed`.
For a skip, `skipReason` says why (`unchanged_hash`, `proposed_only`, `disabled`,
`not_configured`, `drift_preserved`, `out_of_scope`). For a failure, `error`
carries the message.

### Configure

```
POST /notion-sync/:id/configure
```

Provisions the target database and sets the policy — see
[How to connect](#2-configure-pick-a-parent-page--provision-the-database) and
[Configure options](#configure-options). Returns
`{ "connection_id": "...", "config": { ... }, "backfill_enqueued": <n> }`.

### Backfill (force a full re-mirror)

```
POST /notion-sync/:id/backfill
```

Queues every current document for re-mirroring. Unchanged documents are skipped
cheaply, so it is safe to run repeatedly. Returns
`{ "connection_id": "...", "enqueued": <n> }`. The sync must already be configured
(a parent page designated) or the call returns a `400`.

### Unsync (stop mirroring, keep the Notion pages)

```
POST /notion-sync/:id/unsync
```

Disables the sync and clears the page-mapping ledger so a later reconfigure starts
clean. **Your existing Notion pages are left in place** — their data is preserved;
this only stops future syncing. Returns
`{ "connection_id": "...", "disabled": true, "mappings_cleared": <n> }`. To remove
the connection entirely, use `DELETE /connections/:id` (see
[Connecting data sources](connecting-data-sources.md)).

## Via MCP

MCP clients see the Notion sync as three **read-only resources**. The writes
(configure / backfill / unsync) are **not** on MCP by design — configuring where a
workspace exports its knowledge base is workspace configuration, the same posture
as connecting and disconnecting a data source. A human performs those from the
console or your app calls the REST endpoints.

| Resource                    | What it returns                                                                                |
| --------------------------- | --------------------------------------------------------------------------------------------- |
| `notion_sync://<id>`        | The sync config + mirrored-document count.                                                    |
| `notion_sync://<id>/status` | The live status: enabled, provisioned, provisioning state, mirrored count, recent-activity summary. |
| `notion_sync://<id>/sends`  | The recent per-document sync ledger (newest-first) + an hourly summary.                       |
| `notion_sync://<id>/pages`  | The Notion pages the integration can access (the parent-page picker).                         |

The `notion_sync://` reads require the `notion_sync:read` scope.

## Limits and notes

- **Notion rate limit.** Notion caps an integration at roughly 3 requests/second
  per workspace. The sync throttles its own request rate to stay under that
  ceiling; genuine rate-limit responses that slip through are retried with
  backoff. In practice this means a large backfill drains steadily rather than all
  at once.
- **Large documents are chunked.** Notion accepts at most 100 blocks per write, so
  a long document is appended in chunks. Over-long text runs are truncated to
  Notion's per-block limit rather than failing the whole document.
- **Human edits are respected under `drift_policy: preserve`.** If someone edits
  the mirrored page in Notion after the last sync, a `preserve` policy keeps their
  edit and records the skip as `drift_preserved`; only an `overwrite` policy
  re-writes the page from Amdahl. The sync recognizes its own writes, so its own
  re-sync is never mistaken for a human edit.
- **Content-unchanged syncs are free.** A re-sync of a document whose content has
  not changed is skipped without touching Notion (`unchanged_hash`).
- **One sync target per workspace.** The Notion sync is single-instance — one
  outbound Notion connection per workspace.
- **If Notion access is revoked**, the connection flips to a needs-reauthorization
  state and syncing pauses; reconnect to resume (see
  [Connection health and statuses](connecting-data-sources.md#connection-health-and-statuses)).
