Skip to main content
Connect a Notion workspace to Cognee through Notion’s OAuth flow, pick the pages and data sources you want covered, and have them become searchable memory in a dataset of that workspace’s own.
This integration is for self-hosted Cognee deployments — you create and own the Notion public integration, so it talks only to your own Cognee backend. To sync Notion from your own script with an internal integration token instead, use the notion_source() dlt connector, which this integration runs underneath.

What It Does

  • One authorization, one workspace. The Notion workspace id becomes the connection’s providerAccountId and the workspace name its accountLabel. A second workspace is a second connection — see Multiple workspaces.
  • Nothing syncs until you pick roots. Ingesting a whole workspace costs LLM tokens, so a fresh connection holds an empty selection and “everything” is not a valid selection.
  • Manual sync. Notion sends no webhooks on this path, so /sync is what updates memory. Each run is incremental and forgets pages that were deleted or moved out of a selected root, as described for the dlt Notion connector.
  • No extra to install. The adapter and notion_source() both use core dependencies.

Setup

1. Create the Notion public integration

At notion.so/profile/integrations, create a public integration (OAuth) and add https://<your-host>/api/v1/integrations/notion/callback as a redirect URI. It must match NOTION_REDIRECT_URI verbatim. Then copy the integration’s OAuth client ID and client secret. When a user connects, Notion asks them which pages to share with the integration. Only shared pages and data sources show up in the picker.

2. Configure Cognee

Add these to your backend’s .env (the same block is in .env.template): NOTION_API_KEY belongs to the notion_source() connector and is ignored by this integration. Every value is checked when it is used, not at boot: a deployment without Notion configured starts normally, and the first call that needs a missing value fails with NOTION_<NAME> is not configured. POST /authorize turns that into a 503 (notion integration is not configured on this server.). Restart the backend after any .env change.

3. Connect

As with the other OAuth connectors, /authorize sets a short-lived cookie the callback checks alongside the signed state, so the install has to finish in the browser that started it, within 10 minutes. After the user approves, Notion redirects to /api/v1/integrations/notion/callback, which binds the workspace to the Cognee user and sends the browser to <NOTION_FRONTEND_BASE_URL>/integrations?notion=<outcome>:

Choosing What to Index

A new connection is stored with an empty selection. Reconnecting a workspace keeps the selection it already had. List what is selectable:
The list holds every live page and data source shared with the integration, except those whose parent is also shared — the parent’s walk already reads them. The response carries them as resources, next to the saved selected list, and each entry looks like this:
The id is page:<id> or data_source:<id>, with the Notion id in one canonical form (32 lowercase hex characters, no dashes). A page without a title is listed as Untitled. Then save a selection:
/folders and /labels are aliases for /resources. A blank id is refused with 422. An entry that is not page:<id> or data_source:<id> fails the next sync. The picker reads Notion’s search API, which answers about 3 requests per second. After 100 result pages or 60 seconds the listing fails with 502 (Could not list notion resources) rather than returning a partial list.
Narrowing a selection deletes data. The next sync after you drop a root retires what it put in the dataset, and an empty selection retires everything the workspace has indexed. Retiring happens only after a run in which no root failed, so a transient error never takes data with it.

Syncing and Status

Connecting does not start a sync. Ask for one:
The sync runs in the background. While a run is already going for that workspace the call returns {"accepted": false} and starts nothing. Each run:
  1. Checks the token with GET /users/me. If Notion answers 401 / unauthorized, the connection is revoked locally and the sync stops. Because Notion usually issues tokens with no expiry, this is the only way a workspace that removed the integration in Notion shows up in Cognee.
  2. Drops nested roots. A selected root that sits under another selected root would be read twice, under two node sets. The sync follows each root’s parent chain, one request per level, and drops it if the chain reaches another selected root. This also catches pages the picker could not hide, such as a page inside a toggle or column block of a shared page. If Notion reports an object on the chain as not shared, the root is kept. Any other lookup error stops the sync, so a flaky lookup never changes what is synced.
  3. Runs one notion_source() per root through remember() with write_disposition="merge" and self_improvement=False. How the walk, rendering, deletions and node sets work, and what it costs, is described in the dlt Notion connector section.
Pages land in one dataset per workspace, named notion_account_<hash>, where <hash> is the first 10 hex characters of the SHA-256 of the workspace id. The workspace name is not part of the name, so renaming the workspace does not move later syncs to a new dataset.
Read the outcome from GET /api/v1/integrations/notion/connection (syncStatus, lastSyncedAt, syncCounts). syncStatus is degraded when any root failed, or when the run stopped before syncing (a failed parent lookup, a malformed selection entry). A failed root is counted under failed_ingestion, or under failed_rate_limit when Notion rate-limited it. Nothing refreshes the dataset on its own, so call /sync again, or from your own scheduler, to pick up changes.

Multiple Workspaces

Each workspace you authorize is its own connection with its own selection and dataset. List them:
Connections are listed newest first. Pass a providerAccountId as account_id to GET /connection, GET and PUT /resources, POST /sync, or DELETE /connection to act on that workspace:
Without account_id the newest connection answers. An account_id that is not one of your active connections returns 404, never another workspace, and a blank one returns 422. GET /api/v1/integrations/status still shows one connection per provider, the newest. The /connections route and account_id parameter belong to the generic /api/v1/integrations/{provider}/* routes, so they work the same way for every OAuth provider.

Token Lifecycle

  • No expiry, no refresh. When Notion’s token response carries no expires_in, the token is stored without an expiry and used as is. The token check at the start of each sync is what catches a revoked one.
  • With an expiry, the access token is refreshed 5 minutes before it expires. Notion rotates the refresh token on every refresh, so refreshes for one workspace run one at a time within a process, and a compare-and-swap on the stored credential covers several processes. If a refresh response carries no new refresh token, the old one is kept.
  • invalid_grant on refresh means the workspace removed Cognee’s access in Notion. The local connection is revoked; reconnect to fix it.
  • A connection with no refresh token cannot refresh and has to reconnect.

Disconnect

Add account_id=<workspace id> to disconnect a specific workspace; without it the newest connection is disconnected. The local revoke cuts access. Cognee also asks Notion to revoke both the access token and the refresh token, best-effort, so a network error never blocks a disconnect. Without delete_data=true the notion_account_… dataset stays; use forget() later if needed.

Limitations

  • No automatic refresh. No webhooks and no scheduler: /sync is the only thing that updates memory.
  • Shared content only. The picker and the sync see only what the workspace shared with the integration.
  • A workspace belongs to one Cognee user. Connecting a workspace another user holds ends in error_already_connected.
  • The notion_source() limits apply, including the request cost of re-walking every root on each run and the 10,000-row cap on a data source. See the dlt Notion connector.
  • One sync per workspace at a time, per process. The running-sync guard lives in the API process.

Troubleshooting

On POST /authorize, one of NOTION_CLIENT_ID, NOTION_REDIRECT_URI or NOTION_STATE_SECRET is unset; the backend log names it (NOTION_<NAME> is not configured). On the callback, NOTION_FRONTEND_BASE_URL is unset, so there is nowhere to send the browser.
Expected until you save a selection and call /sync. Check GET /resources for what is selected and GET /connection for syncStatus and lastSyncedAt.
Its parent is also shared, so it is covered by selecting the parent. Archived and trashed pages are not listed either.
resourceIds was null (Notion needs an explicit list) or contained a blank id.
Notion rejected the token at the start of a sync, or a refresh failed with invalid_grant. Either way the workspace removed Cognee’s access in Notion. The log line is Notion access for workspace <id> was removed at the provider or ... was revoked at the provider. Reconnect.
At least one root failed, or the run stopped before syncing, and deselected roots were not retired in that run. The backend log names the failure; fix it and run /sync again.
The id is not one of your active connections — it is unknown, disconnected, or belongs to another user. List yours with GET /connections.

dlt Notion connector

Sync Notion from your own script with notion_source() and an internal integration token.

Google Drive

The same OAuth, picker and manual-sync flow for Drive folders.