{
  "name": "Answer website questions from a Supabase vector store with an OpenAI chatbot",
  "nodes": [
    {
      "parameters": {
        "content": "## Answer website questions from a Supabase vector store with an OpenAI chatbot\n\nFor teams with a lot of website or help content who want answers grounded in it. The agent searches a Supabase knowledge base instead of one pasted block of text.\n\n### How it works\n1. A webhook receives the widget message and checks a shared secret.\n2. An OpenAI agent searches the Supabase vector store before every answer and says so when nothing relevant is found.\n3. If it cannot help, it takes an email address and calls the request contact tool to email your team.\n\n### Setup\n- [ ] Edit **Chatbot settings**: company name, your knowledge text and a random `webhook_secret`.\n- [ ] Add your OpenAI credential to **GPT-4.1 mini model**.\n- [ ] Create the `documents` table and `match_documents` function in Supabase, then fill it with the Supabase ingestion template.\n- [ ] Add your Supabase and OpenAI credentials to the vector store and embeddings nodes.\n- [ ] Copy each tool section below the main flow into its own new workflow, save it, then select it in the matching tool node.\n- [ ] In each tool workflow, add your SMTP credential and fill in **Tool settings**.\n- [ ] Test with a POST to the webhook Test URL (the n8n chat panel needs a Chat Trigger). Then activate and add the production URL to your widget, with the same secret as `metadata.webhook_secret`.\n\n### Requirements\nn8n, an OpenAI API key, a Supabase project with pgvector and SMTP credentials.\n\n### Customization tips\nPoint the retrieval tool at a different table, change how many results it returns or reword the prompt.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "ddcc9caa-5767-4a08-a625-3bdabefbe948",
      "name": "Template overview"
    },
    {
      "parameters": {
        "content": "## 1. Receive and verify the message\nThe webhook takes the widget message. Requests without your secret are rejected.",
        "width": 944,
        "height": 432,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "abfb97c4-f7b3-4719-98f6-fb2ea1047736",
      "name": "Section 1: Receive and verify the message"
    },
    {
      "parameters": {
        "content": "## 2. Answer with the AI agent\nThe agent answers from your settings, remembers the session and calls its tools when needed.",
        "width": 944,
        "height": 704,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        528
      ],
      "id": "7e5c5096-d27b-478b-b790-f3855d8c174c",
      "name": "Section 2: Answer with the AI agent"
    },
    {
      "parameters": {
        "content": "## 3. Tool: request_contact (copy to its own workflow)\nCopy these nodes into a new workflow and save it. Then select that workflow in the request_contact tool node.",
        "width": 1328,
        "height": 640,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1328
      ],
      "id": "9df213af-b3d3-49f8-8cb6-e8d838ea3d5d",
      "name": "Section 3: Tool: request_contact (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ai-faq-chatbot-vector-store",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "ai-faq-chatbot-vector-store-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Company",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "handoff_email",
              "value": "you@example.com",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "webhook_secret",
              "value": "CHANGE_ME_TO_A_RANDOM_STRING",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "message",
              "value": "={{ $json.body.chatInput || $json.body.message || '' }}",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "session_id",
              "value": "={{ $json.body.sessionId || 'no-session' }}",
              "type": "string"
            },
            {
              "id": "c6",
              "name": "sent_secret",
              "value": "={{ $json.body.metadata?.webhook_secret || (typeof $json.body.metadata === 'string' ? ($json.body.metadata.match(/\"webhook_secret\"\\s*:\\s*\"([^\"]*)\"/) || [])[1] : '') || '' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "config",
      "name": "Chatbot settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        304,
        112
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "sec1",
              "leftValue": "={{ $json.sent_secret }}",
              "rightValue": "={{ $json.webhook_secret }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-secret",
      "name": "Request from your widget?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        544,
        112
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ output: 'Sorry, something went wrong. Please try again later.' }) }}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "reject",
      "name": "Reject unknown requests",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        784,
        272
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are the assistant on {{ $json.company_name }}'s website.\n\nANSWERING\n- Use the knowledge_base tool to search for relevant information before answering. Do not answer from general knowledge.\n- If the tool returns nothing relevant, say you're not sure - do not guess or invent an answer.\n- Never invent pricing, availability, or capability.\n\nCONTACT HANDOFF\n- If you can't answer a question, or the visitor asks to be contacted, offer to take their email and pass it along.\n- Once you have an email, call request_contact with the email, a short summary of what they asked, and session_id \"{{ $json.session_id }}\".\n\nTONE\n- Short. Plain. No exclamation marks."
        }
      },
      "id": "agent",
      "name": "Answer questions from the knowledge base",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.7,
      "position": [
        64,
        640
      ]
    },
    {
      "parameters": {
        "model": "gpt-4.1-mini",
        "options": {}
      },
      "id": "model",
      "name": "GPT-4.1 mini model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        64,
        880
      ]
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $('Chatbot settings').item.json.session_id }}",
        "contextWindowLength": 20
      },
      "id": "memory",
      "name": "Conversation memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [
        304,
        880
      ]
    },
    {
      "parameters": {
        "name": "request_contact",
        "description": "Send an email so a human can follow up. Call once you have the visitor's email and a reason to save it.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_REQUEST_CONTACT_WORKFLOW_ID_HERE",
          "cachedResultName": "request_contact (email)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"email\": \"sam@example.com\",\n  \"summary\": \"Asked about X, wants a callback.\",\n  \"session_id\": \"\"\n}"
      },
      "id": "tool-request-contact",
      "name": "request_contact",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 1.2,
      "position": [
        544,
        880
      ]
    },
    {
      "parameters": {
        "mode": "retrieve-as-tool",
        "toolDescription": "Search the knowledge base for information relevant to the visitor's question. Always call this before answering - do not rely on general knowledge.",
        "tableName": {
          "__rl": true,
          "mode": "list",
          "value": "documents",
          "cachedResultName": "documents"
        },
        "topK": 5,
        "options": {
          "queryName": "match_documents"
        }
      },
      "id": "knowledge-base",
      "name": "knowledge_base",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase",
      "typeVersion": 1.3,
      "position": [
        784,
        880
      ]
    },
    {
      "parameters": {
        "model": "text-embedding-3-small",
        "options": {}
      },
      "id": "embeddings-openai",
      "name": "Create search embeddings",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "typeVersion": 1.2,
      "position": [
        784,
        1080
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ output: $json.output }) }}",
        "options": {}
      },
      "id": "respond",
      "name": "Respond to widget",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        544,
        640
      ]
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "sub-trigger",
      "name": "When called by the agent",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        64,
        1640
      ]
    },
    {
      "parameters": {
        "jsCode": "const q = $input.first().json.query ?? $input.first().json;\nconst d = typeof q === 'string' ? JSON.parse(q) : q;\n\nconst email = String(d.email || '').trim().toLowerCase();\nconst valid = /^[^@\\s]+@[^@\\s]+\\.[^@\\s]{2,}$/.test(email);\n\nreturn [{ json: {\n  valid,\n  email,\n  summary: d.summary || '',\n  session_id: d.session_id || ''\n} }];"
      },
      "id": "parse-input",
      "name": "Parse and validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        284,
        1640
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "v1",
              "leftValue": "={{ $json.valid }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-valid",
      "name": "Valid email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        504,
        1640
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "response",
              "value": "That email address doesn't look valid. Ask them to confirm it, then try again.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "invalid-response",
      "name": "Tell the agent it failed",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        1800
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "handoff_email",
              "value": "you@example.com",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        1500
      ]
    },
    {
      "parameters": {
        "fromEmail": "chatbot@example.com",
        "toEmail": "={{ $json.handoff_email }}",
        "subject": "=New chat contact request",
        "text": "=A visitor asked to be contacted through the chatbot.\n\nEmail: {{ $json.email }}\nSummary: {{ $json.summary }}\nSession: {{ $json.session_id }}",
        "options": {}
      },
      "id": "send-email",
      "name": "Email the visitor details to your team",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        944,
        1500
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "response",
              "value": "Done. Someone will follow up by email shortly.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "success-response",
      "name": "Tell the agent it worked",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1164,
        1440
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "response",
              "value": "Something went wrong sending that on this end. Let the visitor know their email's been noted, but suggest they reach out directly too in case this doesn't land.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "failure-response",
      "name": "Tell the agent the send failed",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1164,
        1600
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Chat webhook": {
      "main": [
        [
          {
            "node": "Chatbot settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chatbot settings": {
      "main": [
        [
          {
            "node": "Request from your widget?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request from your widget?": {
      "main": [
        [
          {
            "node": "Answer questions from the knowledge base",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject unknown requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer questions from the knowledge base": {
      "main": [
        [
          {
            "node": "Respond to widget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4.1 mini model": {
      "ai_languageModel": [
        [
          {
            "node": "Answer questions from the knowledge base",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Conversation memory": {
      "ai_memory": [
        [
          {
            "node": "Answer questions from the knowledge base",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "request_contact": {
      "ai_tool": [
        [
          {
            "node": "Answer questions from the knowledge base",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "knowledge_base": {
      "ai_tool": [
        [
          {
            "node": "Answer questions from the knowledge base",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Create search embeddings": {
      "ai_embedding": [
        [
          {
            "node": "knowledge_base",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "When called by the agent": {
      "main": [
        [
          {
            "node": "Parse and validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and validate": {
      "main": [
        [
          {
            "node": "Valid email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid email?": {
      "main": [
        [
          {
            "node": "Tool settings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the agent it failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool settings": {
      "main": [
        [
          {
            "node": "Email the visitor details to your team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email the visitor details to your team": {
      "main": [
        [
          {
            "node": "Tell the agent it worked",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the agent the send failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}