This workflow corresponds to n8n.io template #16182 — we link there as the canonical source.
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 →
{
"meta": {
"templateCredsSetupCompleted": false
},
"name": "Answer business FAQs with Claude RAG using Supabase pgvector and OpenAI embeddings",
"tags": [],
"nodes": [
{
"id": "a1b2c3d4-0007-4000-8000-000000000001",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
48
],
"parameters": {
"width": 480,
"height": 896,
"content": "## Answer business FAQs with Claude RAG using Supabase pgvector and OpenAI embeddings\n\n### How it works\n\nThis workflow exposes a webhook that accepts a question from a chat UI, a Slack bot, or any frontend. It embeds the question with OpenAI text-embedding-3-small, searches a Supabase pgvector documents table for the most semantically similar chunks of your knowledge base, builds a context block from the top matches with citation markers, and sends the context plus question to Anthropic Claude with a strict cite-only-from-context system instruction. Claude returns a JSON object containing the answer and the chunk IDs it cited. The workflow returns the answer and citations in the webhook response. The result is a free-stack, Claude-led RAG endpoint with zero managed-vector-DB dependency.\n\n### Setup steps\n\n- Create a Supabase project. In the SQL editor, enable the pgvector extension and create a documents table with columns id, content, metadata, and embedding (vector dimension 1536), then create a match_documents RPC function (full SQL provided in the description).\n- Populate the documents table by chunking your knowledge base content and inserting rows with their embeddings; the ingestion side is a separate workflow.\n- Add an HTTP Header Auth credential for OpenAI with header name Authorization and value Bearer sk-...; attach to the Generate Question Embedding node.\n- Add an HTTP Header Auth credential for Supabase with header name apikey and value of your anon key, plus a second header Authorization Bearer with the same anon key; attach to the Search Supabase pgvector node.\n- Add an HTTP Header Auth credential for Anthropic with header name x-api-key and value sk-ant-...; attach to the Generate Answer with Claude node.\n- Edit Set Search Configuration with your Supabase project URL, embedding model, top_k, and similarity threshold.\n\n### Customization\n\nReplace OpenAI embeddings with Voyage AI, Cohere, or a self-hosted embedder by changing the URL and body in Generate Question Embedding. Swap Supabase pgvector for Pinecone, Qdrant, Weaviate, or Postgres native by changing the URL and body in Search Supabase pgvector. Add conversation memory by inserting a Postgres or Sheets read step that loads the prior turns of the session before Generate Answer with Claude. Pipe the answer into Slack, Telegram, WhatsApp, or a web chat widget by adding a delivery node in parallel to Respond with Answer."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000002",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
192,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Receive and configure\n\nAccepts a question via webhook and standardizes the search parameters (project URL, top_k, similarity threshold) used by the embedding and vector search nodes."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000003",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Embed and search\n\nGenerates an OpenAI embedding for the question and runs a similarity search against the Supabase pgvector documents table to retrieve the top relevant chunks."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000004",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1088,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Build context and answer\n\nAssembles the retrieved chunks into a citation-marked context block, then sends the question plus context to Claude with a strict cite-only-from-context rubric."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000005",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1536,
144
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## Format and respond\n\nParses Claude's JSON, extracts cited chunk IDs, attaches source metadata, and returns the structured answer in the webhook response."
},
"typeVersion": 1
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000006",
"name": "Receive Question Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
240,
320
],
"parameters": {
"path": "faq-bot",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000007",
"name": "Set Search Configuration",
"type": "n8n-nodes-base.set",
"position": [
460,
320
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "f1-question",
"name": "question",
"type": "string",
"value": "={{ ($json.body?.question || $json.question || '').toString().trim() }}"
},
{
"id": "f2-session",
"name": "sessionId",
"type": "string",
"value": "={{ $json.body?.session_id || $json.session_id || '' }}"
},
{
"id": "f3-supabaseUrl",
"name": "supabaseUrl",
"type": "string",
"value": "REPLACE_WITH_YOUR_SUPABASE_PROJECT_URL"
},
{
"id": "f4-embedModel",
"name": "embeddingModel",
"type": "string",
"value": "text-embedding-3-small"
},
{
"id": "f5-topK",
"name": "topK",
"type": "number",
"value": 5
},
{
"id": "f6-threshold",
"name": "matchThreshold",
"type": "number",
"value": 0.7
},
{
"id": "f7-businessName",
"name": "businessName",
"type": "string",
"value": "REPLACE_WITH_YOUR_BUSINESS_NAME"
},
{
"id": "f8-ts",
"name": "askedAt",
"type": "string",
"value": "={{ $now.toISO() }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000008",
"name": "Generate Question Embedding",
"type": "n8n-nodes-base.httpRequest",
"position": [
680,
320
],
"parameters": {
"url": "https://api.openai.com/v1/embeddings",
"method": "POST",
"options": {},
"jsonBody": "={\n \"model\": \"{{ $json.embeddingModel }}\",\n \"input\": {{ JSON.stringify($json.question) }}\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000009",
"name": "Search Supabase pgvector",
"type": "n8n-nodes-base.httpRequest",
"position": [
900,
320
],
"parameters": {
"url": "={{ $('Set Search Configuration').item.json.supabaseUrl }}/rest/v1/rpc/match_documents",
"method": "POST",
"options": {},
"jsonBody": "={\n \"query_embedding\": {{ JSON.stringify($json.data[0].embedding) }},\n \"match_threshold\": {{ $('Set Search Configuration').item.json.matchThreshold }},\n \"match_count\": {{ $('Set Search Configuration').item.json.topK }}\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000010",
"name": "Build Context from Matches",
"type": "n8n-nodes-base.code",
"position": [
1120,
320
],
"parameters": {
"jsCode": "// Assemble retrieved chunks into a citation-marked context block for Claude\nconst config = $('Set Search Configuration').item.json;\nconst raw = $json;\nconst matches = Array.isArray(raw) ? raw : (raw.data || raw.matches || []);\n\nif (matches.length === 0) {\n return [{\n json: {\n question: config.question,\n sessionId: config.sessionId,\n askedAt: config.askedAt,\n businessName: config.businessName,\n contextBlock: '(No matching documents found in the knowledge base.)',\n sources: [],\n noContext: true\n }\n }];\n}\n\nconst chunks = matches.slice(0, config.topK || 5).map((m, i) => {\n const id = (m.id != null ? m.id : ('chunk_' + (i + 1))).toString();\n const score = Number(m.similarity || m.score || 0);\n const content = (m.content || m.text || '').toString();\n const meta = m.metadata || {};\n return { id, score, content, metadata: meta };\n});\n\nconst contextBlock = chunks.map(c => `[${c.id}]\\n${c.content}`).join('\\n\\n---\\n\\n');\n\nreturn [{\n json: {\n question: config.question,\n sessionId: config.sessionId,\n askedAt: config.askedAt,\n businessName: config.businessName,\n contextBlock,\n sources: chunks.map(c => ({ id: c.id, score: c.score, title: c.metadata.title || c.metadata.source || '' })),\n noContext: false\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000011",
"name": "Generate Answer with Claude",
"type": "n8n-nodes-base.httpRequest",
"position": [
1340,
320
],
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"options": {},
"jsonBody": "={\n \"model\": \"claude-sonnet-4-6\",\n \"max_tokens\": 800,\n \"system\": \"You are a customer support assistant for \" + {{ JSON.stringify($json.businessName) }} + \". Answer using ONLY the information in the CONTEXT block. If the answer is not in the context, say so plainly; do not invent facts, do not speculate, do not pull from general knowledge. Cite every claim by including the chunk id in square brackets, e.g. [chunk_3], at the end of the sentence that uses it. Reply with valid JSON only, no markdown fences, no commentary. Schema:\\n\\n{\\n \\\"answer\\\": \\\"<the response, with [chunk_id] citations inline>\\\",\\n \\\"citations\\\": [\\\"<chunk_id 1>\\\", \\\"<chunk_id 2>\\\"],\\n \\\"confident\\\": <true if the answer is fully supported by context, false otherwise>\\n}\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"CONTEXT:\\n\" + {{ JSON.stringify($json.contextBlock) }} + \"\\n\\nQUESTION:\\n\" + {{ JSON.stringify($json.question) }}\n }\n ]\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000012",
"name": "Parse and Format Answer",
"type": "n8n-nodes-base.code",
"position": [
1580,
320
],
"parameters": {
"jsCode": "// Parse Claude's JSON, attach source metadata for cited chunks, build final response\nconst ctx = $('Build Context from Matches').item.json;\nconst raw = $json.content?.[0]?.text || '{}';\n\nlet parsed = {\n answer: 'AI response could not be parsed.',\n citations: [],\n confident: false\n};\n\ntry {\n const cleaned = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```\\s*$/i, '').trim();\n parsed = JSON.parse(cleaned);\n} catch (e) {\n parsed.answer = 'AI returned malformed JSON: ' + raw.slice(0, 200);\n}\n\nconst citedSources = (Array.isArray(parsed.citations) ? parsed.citations : [])\n .map(id => ctx.sources.find(s => s.id.toString() === id.toString()))\n .filter(Boolean);\n\nreturn [{\n json: {\n question: ctx.question,\n sessionId: ctx.sessionId,\n askedAt: ctx.askedAt,\n answer: parsed.answer,\n confident: parsed.confident === true,\n sources: citedSources,\n allRetrievedSources: ctx.sources,\n noContext: ctx.noContext\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a1b2c3d4-0007-4000-8000-000000000013",
"name": "Respond with Answer",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1800,
320
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ { ok: true, question: $json.question, answer: $json.answer, confident: $json.confident, sources: $json.sources, session_id: $json.sessionId } }}"
},
"typeVersion": 1.1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Parse and Format Answer": {
"main": [
[
{
"node": "Respond with Answer",
"type": "main",
"index": 0
}
]
]
},
"Receive Question Webhook": {
"main": [
[
{
"node": "Set Search Configuration",
"type": "main",
"index": 0
}
]
]
},
"Search Supabase pgvector": {
"main": [
[
{
"node": "Build Context from Matches",
"type": "main",
"index": 0
}
]
]
},
"Set Search Configuration": {
"main": [
[
{
"node": "Generate Question Embedding",
"type": "main",
"index": 0
}
]
]
},
"Build Context from Matches": {
"main": [
[
{
"node": "Generate Answer with Claude",
"type": "main",
"index": 0
}
]
]
},
"Generate Answer with Claude": {
"main": [
[
{
"node": "Parse and Format Answer",
"type": "main",
"index": 0
}
]
]
},
"Generate Question Embedding": {
"main": [
[
{
"node": "Search Supabase pgvector",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow exposes a webhook-based FAQ endpoint that embeds incoming questions with OpenAI, retrieves relevant knowledge base chunks from Supabase pgvector, and asks Anthropic Claude to answer strictly from that context with citations, returning a structured JSON response.…
Source: https://n8n.io/workflows/16182/ — 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.
Agente IA con ChromaDB + Ollama + API Empresa. Uses httpRequest, functionItem. Webhook trigger; 8 nodes.
Chatbot-Query-Qdrant. Uses httpRequest, postgres. Webhook trigger; 7 nodes.
Kreativ: RAG Ingestion Pipeline. Uses httpRequest, postgres. Webhook trigger; 5 nodes.
PRAGMAS - Ingest Paper. Uses executeCommand, httpRequest. Webhook trigger; 4 nodes.