n8n workflow template
Notion RAG - Index & Query Workflow
Pulls pages from a Notion database on a schedule, extracts and chunks their markdown content, embeds each chunk with OpenAI, and upserts the vectors into a Supabase table for later semantic search. Includes an error-catching sub-flow for failure alerting.
Advanced45-90 minutesn8nNotionSupabaseOpenAI
01
What this workflow handles
- Automated daily re-indexing of a Notion knowledge base
- Chunked, embedded content ready for vector search
- Failure alerting separate from the main customer-facing flow
02
Setup steps
- 1Connect Notion, OpenAI, and Supabase credentials
- 2Set your Notion database ID
- 3Create a Supabase table with a vector column (pgvector) matching the schema used here
- 4Point the Daily Reindex schedule to your desired frequency
- 5Wire the "Send Alert" placeholder node to Slack/Email/Telegram
03
Values to replace
Review every placeholder below before activating this workflow. Public downloads should never include live credentials or client data.
- Notion database ID
- Supabase project URL
- Supabase table name
- OpenAI credential
Workflow JSON preview
Review before downloading or importing.
{
"name": "Notion RAG - Index (KB Creation)",
"nodes": [
{
"parameters": {
"content": "## What this workflow does\n\nOn a schedule, pulls every page from a Notion database, converts each page to markdown, chunks it, embeds each chunk with OpenAI, and upserts the vectors into a Supabase table for RAG retrieval (see the companion `search_kb` workflow).\n\n**Setup:**\n1. Connect your Notion, OpenAI, and Supabase credentials.\n2. Set your Notion database ID below.\n3. Point the Supabase insert step at your own vector table.\n4. (Optional) Import the companion 'AI Agent - Error Handler' workflow and set it as this workflow's Error Workflow in Settings, so failures get logged/alerted instead of failing silently.",
"height": 280,
"width": 460
},
"id": "4b796c1e-0780-4884-8da4-933e5a4c8e46",
"name": "Purpose",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
-416
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
]
}
},
"id": "ef16b555-b7f8-4cce-8ffe-9ee65e192cc0",
"name": "Daily Reindex",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
160,
-112
]
},
{
"parameters": {
"resource": "databasePage",
"operation": "getAll",
"databaseId": {
"__rl": true,
"value": "YOUR_NOTION_DATABASE_ID",
"mode": "list",
"cachedResultName": "Docs",
"cachedResultUrl": "https://app.notion.com/YOUR_NOTION_DATABASE_ID"
},
"returnAll": true,
"options": {}
},
"id": "255485a4-74a7-4eb3-9315-07fc47cd2ee6",
"name": "Notion: Get Database Pages",
"type": "n8n-nodes-base.notion",
"typeVersion": 2.2,
"position": [
480,
-112
],
"credentials": {
"notionApi": {
"id": "YOUR_CREDENTIAL_ID",
"name": "Notion account"
}
}
},
{
"parameters": {
"jsCode": "// The current item's markdown page data\nconst page = $input.first().json;\n\n// markdown is already a plain text string, not a block array — just use it directly\nconst full_text = (page.markdown || '').trim();\n\n// Extract the title from the first Markdown heading line (e.g. \"# Branch Opening Hours\")\nlet title = 'Untitled';\nconst titleMatch = full_text.match(/^#\\s+(.+)$/m);\nif (titleMatch) {\n title = titleMatch[1].trim();\n}\n\n// Rebuild the Notion URL from the page id (matches the app.notion.com/p/<id-no-dashes> format)\nconst rawId = page.id || '';\nconst notion_url = rawId ? `https://app.notion.com/p/${rawId.replace(/-/g, '')}` : '';\n\nreturn [{\n json: {\n full_text,\n title,\n notion_url,\n page_id: rawId\n }\n}];"
},
"id": "6787ca67-3e4c-4695-8f1b-8cdc2614c1c5",
"name": "Extract Text From Blocks",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1760,
-96
]
},
{
"parameters": {
"jsCode": "// Split the page's full text into ~500-char chunks with 100-char overlap.\n// Tag each chunk with its source page title, URL, and page ID for later citation.\nconst { full_text, title, notion_url, page_id } = $json;\n\nconst CHUNK_SIZE = 30000;\nconst OVERLAP = 2000;\n\nconst text = full_text || '';\nif (!text.trim()) {\n return [];\n}\n\nconst chunks = [];\nlet start = 0;\nwhile (start < text.length) {\n const end = Math.min(start + CHUNK_SIZE, text.length);\n chunks.push(text.slice(start, end));\n if (end === text.length) break;\n start = end - OVERLAP;\n}\n\nreturn chunks.map((chunk_text, idx) => ({\n json: {\n chunk_text,\n chunk_index: idx,\n title,\n notion_url,\n page_id\n }\n}));"
},
"id": "26c0e304-f2f4-41b7-95a0-43407bf69b90",
"name": "Chunk Text",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2032,
-96
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/embeddings",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"text-embedding-3-small\",\n \"input\": {{ JSON.stringify($json.chunk_text) }}\n}",
"options": {
"batching": {
"batch": {
"batchSize": 0
}
}
}
},
"id": "6aebee54-331e-479a-871b-f63010cef765",
"name": "OpenAI: Create Embedding",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
2592,
-80
],
"retryOnFail": true,
"credentials": {
"openAiApi": {
"id": "YOUR_CREDENTIAL_ID",
"name": "OpenAI account"
}
}
},
{
"parameters": {
"jsCode": "// Merge the returned embedding vector with the chunk's text + metadata for the Supabase insert.\nconst emb = $json.data?.[0]?.embedding || [];\nconst chunk = $('Extract Text From Blocks').first().json.full_text;\n\nreturn [{\n json: {\n content: chunk.chunk_text,\n embedding: emb,\n metadata: {\n title: chunk.title,\n notion_url: chunk.notion_url,\n page_id: chunk.page_id,\n chunk_index: chunk.chunk_index\n }\n }\n}];"
},
"id": "50a189a8-4c3d-4750-ab1a-ff07a27c77a7",
"name": "Prepare Supabase Row",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3168,
-64
]
},
{
"parameters": {
"method": "POST",
"url": "LINK TO YOUR SUPABASE TABLE HERE",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Prefer",
"value": "resolution=merge-duplicates"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"content\": {{ JSON.stringify($('Extract Text From Blocks').item.json.full_text) }},\n \"embedding\": {{ JSON.stringify($json.embedding) }}\n}",
"options": {}
},
"id": "73587406-074f-405f-b314-84525605fb32",
"name": "Supabase: Insert Row",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
3616,
-64
],
"retryOnFail": true,
"credentials": {
"supabaseApi": {
"id": "YOUR_CREDENTIAL_ID",
"name": "Supabase account"
}
}
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1488,
-112
],
"id": "f01e8c2e-2a9c-41f6-9ece-76c6360d1355",
"name": "Loop Over Items"
},
{
"parameters": {
"operation": "getMarkdown",
"pageId": {
"__rl": true,
"value": "={{ $json.id }}",
"mode": "id"
}
},
"type": "n8n-nodes-base.notion",
"typeVersion": 3,
"position": [
736,
-112
],
"id": "345bbe7a-6946-4b85-a5b2-1bad010199b2",
"name": "Get page markdown",
"credentials": {
"notionApi": {
"id": "YOUR_CREDENTIAL_ID",
"name": "Notion account"
}
}
},
{
"parameters": {
"operation": "getMarkdown",
"pageId": {
"__rl": true,
"value": "=https://app.notion.com/p/YOUR_NOTION_PAGE_ID",
"mode": "url"
}
},
"type": "n8n-nodes-base.notion",
"typeVersion": 3,
"position": [
1264,
-112
],
"id": "37b86d59-19c0-4df3-b62d-d6abfb4d53db",
"name": "Get page data",
"credentials": {
"notionApi": {
"id": "YOUR_CREDENTIAL_ID",
"name": "Notion account"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst results = [];\n\nconst pageRegex = /<page url=\"([^\"]*)\">([\\s\\S]*?)<\\/page>/g;\n\nfor (const item of items) {\n const markdown = item.json.markdown || '';\n let match;\n\n while ((match = pageRegex.exec(markdown)) !== null) {\n results.push({\n json: {\n parent_id: item.json.id,\n url: match[1],\n content: match[2].trim(),\n object: item.json.object,\n request_id: item.json.request_id\n }\n });\n }\n}\n\nreturn results;"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
976,
-112
],
"id": "e31fe61b-a052-41a4-ae54-a2365e44fff8",
"name": "list pages"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "b49671db-cb62-43c8-a8c1-0cd6b54e4187",
"leftValue": "={{ $json.title }}",
"rightValue": "={{ $('Get row(s)').item.json.title }}",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2320,
-96
],
"id": "221bb943-2a08-4677-b41f-b1778842715a",
"name": "If1"
},
{
"parameters": {},
"type": "n8n-nodes-base.limit",
"typeVersion": 1,
"position": [
960,
-272
],
"id": "c2d60881-7b46-4e55-a3ad-4bb56b37d8f9",
"name": "Limit"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
2864,
-80
],
"id": "c1a9fc6b-942d-47d4-902a-7aea97e38484",
"name": "Loop Over Items1"
}
],
"pinData": {},
"connections": {
"Daily Reindex": {
"main": [
[
{
"node": "Notion: Get Database Pages",
"type": "main",
"index": 0
}
]
]
},
"Notion: Get Database Pages": {
"main": [
[
{
"node": "Get page markdown",
"type": "main",
"index": 0
}
]
]
},
"Extract Text From Blocks": {
"main": [
[
{
"node": "Chunk Text",
"type": "main",
"index": 0
}
]
]
},
"Chunk Text": {
"main": [
[
{
"node": "If1",
"type": "main",
"index": 0
}
]
]
},
"OpenAI: Create Embedding": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
]
]
},
"Prepare Supabase Row": {
"main": [
[
{
"node": "Supabase: Insert Row",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Insert Row": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
],
[]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Extract Text From Blocks",
"type": "main",
"index": 0
}
]
]
},
"Get page markdown": {
"main": [
[
{
"node": "list pages",
"type": "main",
"index": 0
}
]
]
},
"Get page data": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"list pages": {
"main": [
[
{
"node": "Limit",
"type": "main",
"index": 0
}
]
]
},
"If1": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
],
[
{
"node": "OpenAI: Create Embedding",
"type": "main",
"index": 0
}
]
]
},
"Limit": {
"main": [
[
{
"node": "Get page data",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items1": {
"main": [
[],
[
{
"node": "Prepare Supabase Row",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"meta": {
"templateCredsSetupCompleted": false
},
"nodeGroups": [],
"id": "46han6oBPjTxx32v",
"tags": []
}