{
  "name": "Save website chatbot leads to Airtable using OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Save website chatbot leads to Airtable using OpenAI\n\nFor small teams that keep leads in Airtable and want website chatbot conversations added automatically, without duplicates.\n\n### How it works\n1. A webhook receives the widget message and checks a shared secret.\n2. An OpenAI agent answers from your knowledge text and asks for an email when there is a reason.\n3. The capture lead tool validates the email and upserts a record matched on email, storing name, interest, summary, session, page and a consent checkbox.\n4. Calling it again later updates the same record.\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- [ ] 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 Airtable 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 and an Airtable token with access to your base.\n\n### Customization tips\nAdd fields to the table and the upsert mapping, or change what the agent asks for.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "bdd73a40-41c0-43b0-9c10-31dd2f444f88",
      "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": "19af7c90-ce6a-4ecb-ba59-f3f29f166e6a",
      "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": 704,
        "height": 512,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        528
      ],
      "id": "b805fbe3-4fb3-4fad-8385-3b6391c6acab",
      "name": "Section 2: Answer with the AI agent"
    },
    {
      "parameters": {
        "content": "## 3. Tool: capture_lead (copy to its own workflow)\nCopy these nodes into a new workflow and save it. Then select that workflow in the capture_lead tool node.",
        "width": 1104,
        "height": 576,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1136
      ],
      "id": "b7d9bff4-40d9-46e0-a112-622817df1612",
      "name": "Section 3: Tool: capture_lead (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "airtable-lead-capture",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "airtable-lead-capture-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Company",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "knowledge",
              "value": "PASTE YOUR FAQ OR SERVICES COPY HERE.\n\nThe bot answers only from this text.",
              "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": "page_url",
              "value": "={{ $json.body.metadata?.page_url || '' }}",
              "type": "string"
            },
            {
              "id": "c7",
              "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. Answer visitor questions accurately and, where appropriate, capture their details in the lead base.\n\nWHAT YOU KNOW\n{{ $json.knowledge }}\n\nANSWERING\n- Answer only from the information above. Never invent pricing, availability, or capability.\n\nCAPTURING DETAILS\n- Do not ask for an email in your first reply. Answer their question first.\n- Ask only when there is a reason: they cannot be answered, want a demo, or ask to be contacted.\n- Ask for one thing at a time: email, then name, then (optionally) what they're interested in.\n- Once you have at least an email, call capture_lead. Safe to call again later if you learn more.\n\nCONSENT\n- Once you have their email, ask once, as its own separate message and not bundled with anything else, whether it's okay to keep their details on file so someone can follow up.\n- Only set consent_given to true in capture_lead if they clearly say yes. If they haven't been asked yet, or the answer is unclear, leave consent_given false - it defaults to false either way, so silence needs no special handling.\n\nTAGS - choose only from these values\n- interest: pricing, services, integrations, demo, support, partnership, careers\n\nAlways pass session_id \"{{ $json.session_id }}\" and page_url \"{{ $json.page_url }}\".\n\nTONE\n- Short. Plain. No exclamation marks."
        }
      },
      "id": "agent",
      "name": "Answer questions and capture leads",
      "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": "capture_lead",
        "description": "Save a lead to the Airtable base. Call once you have at least an email. Safe to call again later in the same conversation if you learn more - it updates the existing record rather than duplicating it.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_CAPTURE_LEAD_WORKFLOW_ID_HERE",
          "cachedResultName": "capture_lead (Airtable)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"email\": \"sam@example.com\",\n  \"name\": \"Sam\",\n  \"interest\": \"services\",\n  \"consent_given\": false,\n  \"summary\": \"Asked about X. Wants a callback.\",\n  \"session_id\": \"\",\n  \"page_url\": \"\"\n}"
      },
      "id": "tool-capture",
      "name": "capture_lead",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 1.2,
      "position": [
        544,
        880
      ]
    },
    {
      "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,
        1388
      ]
    },
    {
      "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  name: d.name || '',\n  interest: d.interest || '',\n  consent_given: d.consent_given === true || d.consent_given === 'true',\n  summary: d.summary || '',\n  session_id: d.session_id || '',\n  page_url: d.page_url || ''\n} }];"
      },
      "id": "parse-input",
      "name": "Parse and validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        284,
        1388
      ]
    },
    {
      "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,
        1388
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "response",
              "value": "That email address doesn't look valid. Ask them to confirm it, then try again. Nothing has been saved.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "invalid-response",
      "name": "Tell the agent it failed",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        1548
      ]
    },
    {
      "parameters": {
        "resource": "record",
        "operation": "upsert",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "PASTE_YOUR_AIRTABLE_BASE_ID_HERE"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "PASTE_YOUR_AIRTABLE_TABLE_ID_HERE"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "value": {
            "Email": "={{ $json.email }}",
            "Name": "={{ $json.name }}",
            "Interest": "={{ $json.interest }}",
            "Summary": "={{ $json.summary }}",
            "Session ID": "={{ $json.session_id }}",
            "Page URL": "={{ $json.page_url }}",
            "Consent Given": "={{ $json.consent_given }}"
          }
        },
        "options": {}
      },
      "id": "upsert-record",
      "name": "Upsert lead record",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        724,
        1248
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "response",
              "value": "Saved to Airtable.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "success-response",
      "name": "Tell the agent it worked",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        944,
        1248
      ]
    }
  ],
  "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 and capture leads",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject unknown requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer questions and capture leads": {
      "main": [
        [
          {
            "node": "Respond to widget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4.1 mini model": {
      "ai_languageModel": [
        [
          {
            "node": "Answer questions and capture leads",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Conversation memory": {
      "ai_memory": [
        [
          {
            "node": "Answer questions and capture leads",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "capture_lead": {
      "ai_tool": [
        [
          {
            "node": "Answer questions and capture leads",
            "type": "ai_tool",
            "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": "Upsert lead record",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the agent it failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert lead record": {
      "main": [
        [
          {
            "node": "Tell the agent it worked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}