Moonwalk Projects logoMoonwalk Projects
Back to templates

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

  1. 1Connect Notion, OpenAI, and Supabase credentials
  2. 2Set your Notion database ID
  3. 3Create a Supabase table with a vector column (pgvector) matching the schema used here
  4. 4Point the Daily Reindex schedule to your desired frequency
  5. 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": []
}