This workflow corresponds to n8n.io template #16370 — we link there as the canonical source.
This workflow follows the HTTP Request → Postgres recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "VHF3xt11ExE3JiQW",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Reindex Markdown Docs into a Supabase pgvector RAG Store (Schedule + Webhook)",
"tags": [],
"nodes": [
{
"id": "8a1f68ea-817f-425d-afdc-8c0ab1c33ddb",
"name": "Schedule: Daily Reindex",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-48,
160
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 2
}
]
}
},
"typeVersion": 1.3
},
{
"id": "b9cca7ec-ab1e-4afc-9d68-94fd1f3e3e77",
"name": "Fetch Sources (FAQ + Blog)",
"type": "n8n-nodes-base.httpRequest",
"position": [
176,
272
],
"parameters": {
"url": "https://kairesume.fit/api/rag/sources",
"options": {
"timeout": 30000,
"response": {
"response": {
"responseFormat": "json"
}
}
},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 4.4
},
{
"id": "149533b2-93a7-4298-a95f-de3f1ba112c1",
"name": "Chunk and Batch",
"type": "n8n-nodes-base.code",
"position": [
400,
272
],
"parameters": {
"jsCode": "const TARGET_CHARS = 2800;\nconst OVERLAP_CHARS = 400;\nconst MIN_LETTERS_PER_CHUNK = 40;\nconst BATCH_SIZE = 32;\n\nfunction stripFrontmatter(md) {\n if (!md.startsWith(\"---\")) return md;\n const end = md.indexOf(\"\\n---\", 3);\n if (end === -1) return md;\n return md.slice(end + 4).replace(/^\\s+/, \"\");\n}\n\nfunction paragraphSplit(text, target) {\n if (text.length <= target) return text.length;\n const window = text.slice(0, target);\n const paragraphCut = window.lastIndexOf(\"\\n\\n\");\n if (paragraphCut > target * 0.6) return paragraphCut + 2;\n const sentenceCut = window.lastIndexOf(\". \");\n if (sentenceCut > target * 0.6) return sentenceCut + 2;\n return target;\n}\n\nfunction subdivideLongSection(text) {\n if (text.length <= TARGET_CHARS) return [text];\n const out = [];\n let i = 0;\n while (i < text.length) {\n const end = i + paragraphSplit(text.slice(i), TARGET_CHARS);\n out.push(text.slice(i, end).trim());\n if (end >= text.length) break;\n i = Math.max(end - OVERLAP_CHARS, i + 1);\n }\n return out;\n}\n\nfunction letterCount(s) {\n return (s.match(/[A-Za-z]/g) || []).length;\n}\n\nfunction chunkMarkdown(doc) {\n const body = stripFrontmatter(doc.content);\n const sections = body.split(/\\n(?=## )/).map(function (s) { return s.trim(); }).filter(Boolean);\n const pieces = [];\n for (const section of sections) {\n for (const piece of subdivideLongSection(section)) pieces.push(piece);\n }\n const out = [];\n let chunkIdx = 0;\n for (const piece of pieces) {\n if (letterCount(piece) < MIN_LETTERS_PER_CHUNK) continue;\n out.push({ source: doc.source, chunk_idx: chunkIdx, content: piece });\n chunkIdx++;\n }\n return out;\n}\n\nconst inputItem = items[0].json;\nconst sources = inputItem.sources || [];\nconst allChunks = [];\nfor (const doc of sources) {\n for (const chunk of chunkMarkdown(doc)) allChunks.push(chunk);\n}\n\nconst runStart = new Date().toISOString();\nconst totalBatches = Math.max(1, Math.ceil(allChunks.length / BATCH_SIZE));\nconst batches = [];\nfor (let i = 0; i < allChunks.length; i += BATCH_SIZE) {\n const batch = allChunks.slice(i, i + BATCH_SIZE);\n batches.push({\n json: {\n batch: batch,\n inputs: batch.map(function (c) { return c.content; }),\n run_start: runStart,\n batch_idx: batches.length,\n total_batches: totalBatches,\n total_chunks: allChunks.length,\n },\n });\n}\n\nreturn batches;\n"
},
"typeVersion": 2
},
{
"id": "ff79fd6e-7c61-4867-86ab-95b659420fbc",
"name": "Delete Stale Chunks",
"type": "n8n-nodes-base.postgres",
"position": [
848,
80
],
"parameters": {
"query": "DELETE FROM public.rag_chunks WHERE updated_at < $1::timestamptz RETURNING source, chunk_idx;",
"options": {
"queryReplacement": "={{ $(\"Chunk and Batch\").first().json.run_start }}"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "61871abe-9ff3-4024-a3e4-b5b939407b70",
"name": "Embed Batch (Supabase)",
"type": "n8n-nodes-base.httpRequest",
"position": [
848,
272
],
"parameters": {
"url": "https://jhccfsyytbkfmdvduvds.supabase.co/functions/v1/embed",
"method": "POST",
"options": {
"timeout": 60000,
"response": {
"response": {
"responseFormat": "json"
}
}
},
"jsonBody": "={{ JSON.stringify({ inputs: $json.inputs }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.4
},
{
"id": "2d212395-3bb1-43eb-89d9-767c258339e8",
"name": "Build Upsert SQL",
"type": "n8n-nodes-base.code",
"position": [
1072,
272
],
"parameters": {
"jsCode": "const embedResponse = items[0].json;\nconst vectors = embedResponse.vectors || [];\n\nconst loopItem = $(\"Loop Batches\").item.json;\nconst batch = loopItem.batch;\nconst runStart = loopItem.run_start;\nconst batchIdx = loopItem.batch_idx;\n\nif (!Array.isArray(batch) || !Array.isArray(vectors) || batch.length !== vectors.length) {\n throw new Error(\"Vector count \" + vectors.length + \" does not match chunk count \" + (batch ? batch.length : 0) + \" for batch \" + batchIdx);\n}\n\nfunction sqlString(s) {\n return \"'\" + String(s).replace(/'/g, \"''\") + \"'\";\n}\n\nfunction vectorLiteral(v) {\n return sqlString(\"[\" + v.map(function (x) { return Number(x).toFixed(6); }).join(\",\") + \"]\");\n}\n\nconst valuesRows = batch.map(function (c, i) {\n return \"(\" + sqlString(c.source) + \", \" + Number(c.chunk_idx) + \", \" + sqlString(c.content) + \", \" + vectorLiteral(vectors[i]) + \"::vector, NOW())\";\n}).join(\",\\n \");\n\nconst sql = \"INSERT INTO public.rag_chunks (source, chunk_idx, content, embedding, updated_at)\\nVALUES\\n \" + valuesRows + \"\\nON CONFLICT (source, chunk_idx) DO UPDATE\\nSET content = EXCLUDED.content,\\n embedding = EXCLUDED.embedding,\\n updated_at = NOW();\";\n\nreturn [{ json: { sql: sql, batch_size: batch.length, run_start: runStart, batch_idx: batchIdx } }];\n"
},
"typeVersion": 2
},
{
"id": "4ba0f657-7da2-4925-be4f-1cde35ffd794",
"name": "Upsert Chunks",
"type": "n8n-nodes-base.postgres",
"position": [
1296,
352
],
"parameters": {
"query": "{{ $json.sql }}",
"options": {
"queryBatching": "single"
},
"operation": "executeQuery"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
},
"typeVersion": 2.6
},
{
"id": "ffd40eb7-91ba-4f6b-94a5-1b738cd26570",
"name": "Loop Batches",
"type": "n8n-nodes-base.splitInBatches",
"position": [
624,
272
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "1837434c-8804-4074-8472-14b47a9d9a79",
"name": "Deploy Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-48,
352
],
"parameters": {
"path": "kairesume-deploy",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "b05c9c6b-c3a6-4cf7-801b-9890c80cf0d4",
"name": "Sticky Note 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-48,
-480
],
"parameters": {
"width": 520,
"height": 520,
"content": "## \ud83d\udd01 RAG Corpus Reindex \u2192 Supabase pgvector\n\n**Who's it for:** Anyone running a retrieval-augmented (RAG) chatbot or search over their own docs who needs the vector store kept in sync automatically.\n\n**What it does:** On a daily schedule (and on a deploy webhook), it fetches your source markdown (FAQ + blog), chunks it on H2 boundaries (~2800 chars, 400 overlap), embeds each batch via a Supabase Edge Function (e.g. gte-small, 384-dim), bulk-UPSERTs the vectors into a Postgres `rag_chunks` table, then deletes stale rows from removed/shrunk sources.\n\n**How it works:**\n1. **Schedule** (daily) or **Deploy Webhook** triggers a run\n2. **Fetch Sources** pulls markdown docs from your API\n3. **Chunk and Batch** splits into ~32-chunk batches\n4. **Loop Batches** \u2192 **Embed Batch** \u2192 **Build Upsert SQL** \u2192 **Upsert Chunks** (ON CONFLICT updates)\n5. After the loop, **Delete Stale Chunks** removes rows older than this run\n\n**Idempotent** \u2014 re-running just refreshes `updated_at`. Runs on free tiers."
},
"typeVersion": 1
},
{
"id": "7a95922a-8886-4ecc-ab79-2e21727f5f04",
"name": "Sticky Note 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
512,
-480
],
"parameters": {
"color": 4,
"width": 460,
"height": 520,
"content": "### \u2699\ufe0f Setup\n**Credentials (select placeholders after import):**\n- **[RAG_API]_HeaderAuth** \u2014 Header Auth for your sources API + Supabase embed function\n- **[SUPABASE]_Postgres** \u2014 Postgres connection to your Supabase database\n\n**Before running:**\n1. In **Fetch Sources**, set the URL to your docs API (returns `{ sources: [{ source, content }] }`)\n2. In **Embed Batch**, set your Supabase Edge Function URL (returns `{ vectors: [...] }`)\n3. Create the table:\n `rag_chunks(source text, chunk_idx int, content text, embedding vector(384), updated_at timestamptz, primary key(source, chunk_idx))`\n4. Adjust the schedule hour / embedding dimension as needed\n\n**Error handling:** the network nodes use **retry-on-fail (3\u00d7)**. Optionally set an *Error Workflow* in workflow settings to get alerts."
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"timezone": "America/New_York",
"binaryMode": "separate",
"errorWorkflow": "Rsh29fD5MByBp7SN",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "34741ee4-690e-47ca-9404-532116c338a0",
"nodeGroups": [],
"connections": {
"Loop Batches": {
"main": [
[
{
"node": "Delete Stale Chunks",
"type": "main",
"index": 0
}
],
[
{
"node": "Embed Batch (Supabase)",
"type": "main",
"index": 0
}
]
]
},
"Upsert Chunks": {
"main": [
[
{
"node": "Loop Batches",
"type": "main",
"index": 0
}
]
]
},
"Deploy Webhook": {
"main": [
[
{
"node": "Fetch Sources (FAQ + Blog)",
"type": "main",
"index": 0
}
]
]
},
"Chunk and Batch": {
"main": [
[
{
"node": "Loop Batches",
"type": "main",
"index": 0
}
]
]
},
"Build Upsert SQL": {
"main": [
[
{
"node": "Upsert Chunks",
"type": "main",
"index": 0
}
]
]
},
"Embed Batch (Supabase)": {
"main": [
[
{
"node": "Build Upsert SQL",
"type": "main",
"index": 0
}
]
]
},
"Schedule: Daily Reindex": {
"main": [
[
{
"node": "Fetch Sources (FAQ + Blog)",
"type": "main",
"index": 0
}
]
]
},
"Fetch Sources (FAQ + Blog)": {
"main": [
[
{
"node": "Chunk and Batch",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
httpHeaderAuthpostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow reindexes Markdown documentation into a Supabase Postgres pgvector table by fetching source docs from an HTTP API, chunking and embedding them via a Supabase Edge Function, upserting the vectors, and deleting stale chunks on a daily schedule or on-demand webhook.…
Source: https://n8n.io/workflows/16370/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
My Workflow. Uses outputParserStructured, httpRequest, lmChatGoogleGemini, chainLlm. Scheduled trigger; 82 nodes.
Search Worflow Docker Complete. Uses documentDefaultDataLoader, textSplitterCharacterTextSplitter, vectorStoreSupabase, embeddingsOllama. Scheduled trigger; 71 nodes.
This workflow automates end-to-end customer journey management by intelligently routing queries through multiple AI models (OpenAI, Claude) based on complexity and context. Designed for customer succe
This workflow implements a self-healing Retrieval-Augmented Generation (RAG) maintenance system that automatically updates document embeddings, evaluates retrieval quality, detects embedding drift, an
This workflow automates end-to-end e-commerce order processing from intake through fulfillment by orchestrating multiple AI-powered validation stages and external system integrations. Designed for e-c