FlowKit

Build a RAG on Your Notion Knowledge Base with n8n, Step by Step

Published 2 August 2026 · 6 min read

Your team's documentation lives in Notion: procedures, meeting notes, product specs, decisions. The problem is not writing, it is finding — Notion's built-in search matches keywords, not answers. RAG (retrieval-augmented generation) closes exactly that gap: the approach, formalized by Lewis et al. in a paper presented at NeurIPS 2020 ("Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks", see on Google Scholar), retrieves the relevant passages from a corpus and hands them to the model at answer time, improving factuality and enabling source citations. Here is how to build that pipeline with n8n, from Notion page extraction to incremental synchronization.

Pipeline architecture

  1. Extraction: the Notion node lists the pages in scope and fetches their blocks.
  2. Conversion: the JSON blocks returned by the Notion API are turned into usable markdown.
  3. Chunking: the text is split following the page structure (headings, sections).
  4. Vectorization: each chunk becomes an embedding and is inserted into Supabase pgvector with its metadata (page, URL, date).
  5. Synchronization: a scheduled workflow detects modified pages via last_edited_time and reindexes only those.

If you have read our guide on RAG over Google Drive, the logic is the same — only the source changes, and with it the source-specific traps: nested blocks, databases, rate limits.

Step 1: extract pages with the Notion node

Prerequisite: a Notion integration with the target pages explicitly shared — our guide on connecting Notion to n8n covers token creation and page sharing. Share only what you intend to index: that is your first relevance and confidentiality filter.

First structural distinction: pages and databases are not extracted the same way.

  • A Notion database is queried with a Database Page → Get Many style operation: each row is a page, with properties (status, tags, dates) that make excellent metadata.
  • A standalone page is read through its blocks: the Block → Get Many operation (or the blocks/{id}/children endpoint) returns child blocks, paginated in batches of 100 with a next_cursor as long as has_more is true.

Second subtlety: nested blocks. A toggle, a bulleted list with sub-items, a column or a callout can contain child blocks (has_children: true) that the parent call does not return. Depending on your node version, an option can fetch nested blocks as well; otherwise you need to recurse over every block flagged has_children. Skip this step and everything tucked inside toggles — often the bulk of an internal FAQ — silently vanishes from your index.

Step 2: convert blocks to markdown

The Notion API does not return text but JSON objects: each block has a type (paragraph, heading_2, bulleted_list_item, code, toggle…) and a rich_text array. A Code node handles the conversion:

const md = items.map(({ json: b }) => {
  const text = (b[b.type]?.rich_text || [])
    .map(t => t.plain_text).join('');
  if (b.type === 'heading_1') return `# ${text}`;
  if (b.type === 'heading_2') return `## ${text}`;
  if (b.type === 'heading_3') return `### ${text}`;
  if (b.type === 'bulleted_list_item') return `- ${text}`;
  if (b.type === 'toggle') return `## ${text}`;
  return text;
}).join('\n\n');
return [{ json: { markdown: md } }];

Preserving heading levels is not cosmetic: it is what makes the structural chunking of the next step possible. Databases embedded inside a page (child_database blocks) deserve dedicated handling: query them as databases and serialize each row into a sentence ("Client: Acme — Status: signed — Amount: …") rather than dropping the block.

Step 3: chunking that follows the Notion structure

Notion gives you what PDFs never do: explicit structure. Use it — split primarily on heading_1 and heading_2 boundaries, keep a toggle's content attached to its title, and never merge two unrelated sections into the same chunk. The general rules (500–1,000 token chunks, overlap, paragraph-level fallback splitting) are covered in our guide to document chunking for RAG.

This choice is also economically sound: a 2024 study by Qu, Tu and Bao ("Is Semantic Chunking Worth the Computational Cost?", see on Google Scholar) shows that expensive embedding-based semantic chunking does not deliver consistent gains over simpler strategies. With Notion, the semantic structure is already there, for free, in the headings — so use it.

Step 4: embeddings, pgvector and metadata

The Supabase Vector Store node (insert mode), wired to an embeddings model, vectorizes and inserts each chunk — the full setup of the documents table and similarity function is described in our RAG with n8n and Supabase guide, and the model choice in our embeddings model comparison.

The detail that will matter most in production: each chunk's metadata.

{
  "notion_page_id": "{{ $json.id }}",
  "title": "{{ $json.name }}",
  "url": "{{ $json.url }}",
  "last_edited": "{{ $json.last_edited_time }}",
  "section": "{{ $json.currentHeading }}"
}

The url (returned by the API for every page) lets the chatbot cite the source Notion page with a clickable link; notion_page_id enables targeted deletion at resync time; last_edited serves as an audit trail.

Step 5: incremental sync with last_edited_time

Reindexing the whole workspace every night wastes API calls and embeddings. The right approach: a Schedule Trigger that queries pages modified since the last run. On a Notion database, the filter is straightforward:

{
  "filter": {
    "timestamp": "last_edited_time",
    "last_edited_time": { "on_or_after": "{{ $json.lastSyncedAt }}" }
  }
}

Two hard-won precautions: Notion rounds last_edited_time to the minute, so build in a small window overlap (and idempotent reindexing) rather than a strict comparison; and for every modified page, apply the delete then insert sequence — first remove all vectors carrying its notion_page_id, then insert the fresh chunks. Otherwise versions pile up and the RAG cites stale content; the full pattern (including page deletions) is covered in our guide on keeping a RAG index up to date. If you would rather start from a ready-made base, our Notion to vector database sync workflow implements exactly this mechanism.

The cross-cutting trap: rate limits

The Notion API tolerates on average around three requests per second; beyond that, it returns 429 with a Retry-After header. And your pipeline multiplies calls: one page means one call for properties, one or more for blocks, plus the recursion over nested blocks. Three countermeasures: enable retry on fail with a delay on the Notion nodes, process pages sequentially (Loop Over Items) rather than in parallel, and space out batches with a Wait node. Initial indexing of a large workspace can take a while — that is normal and harmless, since incremental sync takes over afterwards.

Querying: agent, citations, and what comes next

On the query side, this is a standard RAG: an AI Agent (or a question-answering chain) with the Vector Store as a tool, and a prompt that requires citing source pages via metadata. If your users often search for exact terms — client names, product references — add hybrid search combining vectors and keywords, the refinement with the best effort-to-impact ratio. The RAG Assistant Pack bundles that downstream half ready to plug in: a chatbot with citations, a question-answering API, and the Notion sync from this article.

Key takeaways

  • Extraction: distinguish databases (filterable queries) from pages (blocks paginated by 100), and recursively fetch nested blocks (has_children) — or toggles vanish from the index.
  • Conversion: turn JSON blocks into markdown while preserving headings, which will drive the chunking.
  • Chunking: follow the Notion structure (sections, toggles) instead of costly semantic splitting.
  • Metadata: notion_page_id, url, last_edited — for citations and clean resynchronization.
  • Incremental sync: filter on last_edited_time (minute-rounded), delete-then-insert sequence.
  • Rate limits: roughly three requests per second on average; retry, sequential processing and a Wait node.

FAQ

Frequently asked questions

How long does the initial indexing of a Notion workspace take?

It mostly depends on Notion's API rate limits, around three requests per second on average: each page requires several calls (blocks paginated by 100, plus nested blocks). For a few hundred pages, expect anywhere from several minutes to an hour. It is a one-off job: after that, incremental sync only touches pages that actually changed.

Should I index the whole Notion workspace or only some pages?

Scope it down: share only the useful pages or databases with your Notion integration rather than the entire workspace. You get better retrieval (less noise), faster indexing, and you avoid exposing sensitive content to every chatbot user.

Can I use a vector database other than Supabase pgvector?

Yes, the pipeline is identical with Qdrant, Pinecone or Weaviate: only the Vector Store node changes. Supabase pgvector remains a solid default if you want vectors and metadata in standard SQL, especially for the targeted deletion of a page's chunks during resync.

How can the chatbot cite the original Notion page in its answers?

By storing the page URL in each chunk's metadata at indexing time. At generation time, the prompt instructs the model to mention its sources, and you rebuild clickable Notion links from the metadata of the retrieved chunks. This is essential so users can verify any answer.

Bundle FlowKit Complet

€269