{
  "name": "Capture chatbot leads in ActiveCampaign with tags and consent using OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Capture chatbot leads in ActiveCampaign with tags and consent using OpenAI\n\nFor marketers who want chatbot leads tagged by interest and intent in ActiveCampaign, and only subscribed to a list after explicit consent.\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 saves a lead when the visitor gives an email and a reason.\n3. The capture lead tool syncs the contact, writes custom fields, applies tags for source, interest, intent, objection and consent, and adds a note.\n4. The contact is subscribed to your list only after a separate, explicit yes.\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 ActiveCampaign credential and fill in **Tool settings**.\n- [ ] Run the setup section once to create the custom fields and tags and list your list IDs.\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 ActiveCampaign API URL and key.\n\n### Customization tips\nAdd sector tags in the setup settings, or change the tag vocabulary and prompt.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "64fa81a4-1985-4bcc-97af-7ff7bd209aa2",
      "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": "29d021f7-c659-4c40-bb54-fd8f182c09ee",
      "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": "fc1f027f-1ed8-4475-8f9f-825250107196",
      "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": 3744,
        "height": 672,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1136
      ],
      "id": "cc0f0809-9c0e-43c5-97fa-115e975e2489",
      "name": "Section 3: Tool: capture_lead (copy to its own workflow)"
    },
    {
      "parameters": {
        "content": "## 4. Run once: create ActiveCampaign fields and tags (copy to its own workflow)\nCopy these nodes into a new workflow. Run it once with the manual trigger. Safe to run again.",
        "width": 2640,
        "height": 368,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1904
      ],
      "id": "0dbbb396-09ac-4258-a2a0-bb568e553079",
      "name": "Section 4: Run once: create ActiveCampaign fields and tags (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "activecampaign-lead-capture",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "ac-lead-capture-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Company",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "consent_wording",
              "value": "Want me to add you to our monthly update email as well? One click to unsubscribe any time.",
              "type": "string"
            },
            {
              "id": "c6",
              "name": "knowledge",
              "value": "PASTE YOUR FAQ OR SERVICES COPY HERE.\n\nThe bot answers only from this text. Anything not in here, it will say it does not know.",
              "type": "string"
            },
            {
              "id": "c9",
              "name": "webhook_secret",
              "value": "CHANGE_ME_TO_A_RANDOM_STRING",
              "type": "string"
            },
            {
              "id": "c10",
              "name": "message",
              "value": "={{ $json.body.chatInput || $json.body.message || '' }}",
              "type": "string"
            },
            {
              "id": "c11",
              "name": "session_id",
              "value": "={{ $json.body.sessionId || 'no-session' }}",
              "type": "string"
            },
            {
              "id": "c12",
              "name": "page_url",
              "value": "={{ $json.body.metadata?.page_url || '' }}",
              "type": "string"
            },
            {
              "id": "c13",
              "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. Your job is to answer visitor questions accurately and, where it is genuinely appropriate, capture their details so a human can follow up.\n\nWHAT YOU KNOW\n{{ $json.knowledge }}\n\nANSWERING\n- Answer only from the information above. If it is not there, say you are not sure and offer to have someone follow up.\n- Never invent pricing, availability, timelines, 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 have asked something you cannot answer, they want a demo, or they have asked to be contacted.\n- Ask for one thing at a time. Never present a list of fields.\n\nCONSENT - follow this exactly\n- Taking their email so a human can answer their question is one thing. Adding them to a marketing list is a separate thing needing a separate, explicit yes.\n- When you have an email and a reason to save it, call capture_lead with consent_given set to false.\n- Only afterwards, if the conversation warrants it, ask separately using this wording: \"{{ $json.consent_wording }}\"\n- If and only if they clearly say yes, call capture_lead a second time with consent_given set to true and consent_wording set to exactly the question you asked.\n- Silence, ambiguity, or \"sure whatever\" to a bundled question is NOT consent. Never bundle the two asks.\n\nTAGS - choose only from these values, never invent one\n- interest: pricing, services, integrations, demo, support, partnership, careers\n- intent: hot, warm, browsing\n- objection: price, timing, competitor, technical-fit, authority\n\nWhen calling capture_lead, always include session_id \"{{ $json.session_id }}\" and page_url \"{{ $json.page_url }}\".\n\nTONE\n- Short. Plain. No exclamation marks. Do not say \"Great question!\".\n- If you do not know, say so."
        }
      },
      "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 ActiveCampaign. Call with consent_given false when you first get an email. Call a second time with consent_given true ONLY after a separate explicit yes to marketing. Never bundle the two questions.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_CAPTURE_LEAD_WORKFLOW_ID_HERE",
          "cachedResultName": "capture_lead (ActiveCampaign)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"email\": \"sam@example.com\",\n  \"first_name\": \"Sam\",\n  \"last_name\": \"\",\n  \"company\": \"\",\n  \"consent_given\": false,\n  \"consent_wording\": \"\",\n  \"interest\": \"services\",\n  \"intent\": \"hot\",\n  \"objection\": \"\",\n  \"sector\": \"\",\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,
        1488
      ]
    },
    {
      "parameters": {
        "jsCode": "// Normalise whatever the agent sent us and validate the email.\nconst 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  first_name: d.first_name || '',\n  last_name: d.last_name || '',\n  company: d.company || '',\n  consent_given: d.consent_given === true || d.consent_given === 'true',\n  consent_wording: d.consent_wording || '',\n  interest: d.interest || '',\n  intent: d.intent || 'browsing',\n  objection: d.objection || '',\n  sector: d.sector || '',\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,
        1488
      ]
    },
    {
      "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,
        1488
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "response",
              "value": "That email address does not 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,
        1648
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "ac_base_url",
              "value": "https://YOURACCOUNT.api-us1.com",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "list_id",
              "value": "1",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        1348
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.ac_base_url }}/api/3/fields?limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "resolve-fields",
      "name": "Resolve field IDs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        1348
      ]
    },
    {
      "parameters": {
        "jsCode": "// Field IDs differ in every ActiveCampaign account, so we look them up\n// at runtime rather than making the user hardcode them.\nconst lead = $('Tool settings').first().json;\nconst map = {};\nfor (const f of ($input.first().json.fields || [])) {\n  map[f.title] = f.id;\n}\nreturn [{ json: { ...lead, field_ids: map } }];"
      },
      "id": "map-fields",
      "name": "Map field names to IDs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1164,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.ac_base_url }}/api/3/contact/sync",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contact: { email: $json.email, firstName: $json.first_name, lastName: $json.last_name } }) }}",
        "options": {}
      },
      "id": "sync-contact",
      "name": "Create or update contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1384,
        1348
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build one item per custom field value to write.\nconst lead = $('Map field names to IDs').first().json;\nconst contactId = $input.first().json.contact.id;\nconst ids = lead.field_ids || {};\n\nconst values = {\n  chat_session_id: lead.session_id,\n  chat_first_seen_url: lead.page_url,\n  chat_last_summary: lead.summary\n};\n\nif (lead.consent_given) {\n  values.chat_consent_timestamp = new Date().toISOString();\n  values.chat_consent_wording = lead.consent_wording;\n}\n\nconst out = [];\nfor (const [name, value] of Object.entries(values)) {\n  if (!ids[name] || !value) continue;\n  out.push({ json: {\n    ac_base_url: lead.ac_base_url,\n    contact_id: contactId,\n    body: { fieldValue: { contact: contactId, field: ids[name], value: String(value) } }\n  } });\n}\n\nif (out.length === 0) {\n  return [{ json: { ac_base_url: lead.ac_base_url, contact_id: contactId, skip: true } }];\n}\nreturn out;"
      },
      "id": "build-field-values",
      "name": "Build field values",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1604,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.ac_base_url }}/api/3/fieldValues",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "write-field-values",
      "name": "Write custom fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1824,
        1348
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cg1",
              "leftValue": "={{ $('Map field names to IDs').first().json.consent_given }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "consent-gate",
      "name": "CONSENT GATE",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2044,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Map field names to IDs').first().json.ac_base_url }}/api/3/contactLists",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contactList: { list: $('Map field names to IDs').first().json.list_id, contact: $('Build field values').first().json.contact_id, status: 1 } }) }}",
        "options": {}
      },
      "id": "subscribe",
      "executeOnce": true,
      "name": "Subscribe to list",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2264,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// One item per tag to apply. Closed vocabulary for model-chosen values —\n// interest/intent/objection/consent only ever come from the fixed lists in\n// the system prompt, so anything the model invents outside `allowed` is\n// dropped rather than created in the account.\n//\n// Sector tags are handled separately and deliberately bypass `allowed`:\n// `sector` is not something the model invents, it's read back from\n// operator-configured `sector_tags` (set up once in the Setup workflow), so\n// there is no closed-vocabulary risk here — the vocabulary just lives in a\n// different config value than the one this filter checks.\nconst lead = $('Map field names to IDs').first().json;\nconst contactId = $('Build field values').first().json.contact_id;\n\nconst allowed = new Set([\n  'source:chat',\n  'interest:pricing','interest:services','interest:integrations','interest:demo',\n  'interest:support','interest:partnership','interest:careers',\n  'intent:hot','intent:warm','intent:browsing',\n  'objection:price','objection:timing','objection:competitor',\n  'objection:technical-fit','objection:authority',\n  'consent:explicit-chat','consent:declined'\n]);\n\nconst wanted = ['source:chat'];\nif (lead.interest) wanted.push('interest:' + lead.interest);\nif (lead.intent) wanted.push('intent:' + lead.intent);\nif (lead.objection) wanted.push('objection:' + lead.objection);\nwanted.push(lead.consent_given ? 'consent:explicit-chat' : 'consent:declined');\n\nconst sectorTag = lead.sector ? 'sector:' + String(lead.sector).toLowerCase().trim().replace(/\\s+/g,'-') : null;\n\nconst final = wanted.filter(t => allowed.has(t));\nif (sectorTag) final.push(sectorTag);\n\nreturn final.map(t => ({ json: {\n  ac_base_url: lead.ac_base_url,\n  contact_id: contactId,\n  tag_name: t\n} }));"
      },
      "id": "build-tags",
      "name": "Build tag list",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2484,
        1348
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.ac_base_url }}/api/3/tags?search={{ encodeURIComponent($json.tag_name) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "find-tag",
      "name": "Find tag ID",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2704,
        1348
      ]
    },
    {
      "parameters": {
        "jsCode": "// Match exactly — AC search is a partial match, so 'intent:hot' would also\n// return anything containing that string.\nconst out = [];\nfor (let i = 0; i < $input.all().length; i++) {\n  const req = $('Build tag list').all()[i].json;\n  const tags = $input.all()[i].json.tags || [];\n  const hit = tags.find(t => t.tag === req.tag_name);\n  if (!hit) continue;\n  out.push({ json: {\n    ac_base_url: req.ac_base_url,\n    body: { contactTag: { contact: req.contact_id, tag: hit.id } }\n  } });\n}\nreturn out;"
      },
      "id": "match-tags",
      "name": "Match tag IDs exactly",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2924,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.ac_base_url }}/api/3/contactTags",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "apply-tags",
      "name": "Apply tags",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3144,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Map field names to IDs').first().json.ac_base_url }}/api/3/notes",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ note: { note: $('Map field names to IDs').first().json.summary, relid: $('Build field values').first().json.contact_id, reltype: 'Subscriber' } }) }}",
        "options": {}
      },
      "id": "add-note",
      "executeOnce": true,
      "name": "Attach summary note",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3364,
        1348
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://trackcmp.net/event",
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "actid",
              "value": "YOUR_EVENT_TRACKING_ACTID"
            },
            {
              "name": "key",
              "value": "YOUR_EVENT_TRACKING_KEY"
            },
            {
              "name": "event",
              "value": "={{ 'chat_' + ($('Map field names to IDs').first().json.interest || 'enquiry') }}"
            },
            {
              "name": "eventdata",
              "value": "={{ $('Map field names to IDs').first().json.summary }}"
            },
            {
              "name": "visit",
              "value": "={{ JSON.stringify({ email: $('Map field names to IDs').first().json.email }) }}"
            }
          ]
        },
        "options": {}
      },
      "id": "event-tracking",
      "name": "Event tracking (disabled)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3584,
        1508
      ],
      "disabled": true
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "response",
              "value": "={{ $('Map field names to IDs').first().json.consent_given ? 'Saved. Contact created or updated, tagged, and subscribed to the marketing list with the consent wording recorded.' : 'Saved. Contact created or updated and tagged. NOT subscribed to any marketing list.' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "success-response",
      "executeOnce": true,
      "name": "Tell the agent it worked",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        3584,
        1348
      ]
    },
    {
      "parameters": {},
      "id": "setup-trigger",
      "name": "Click to run setup",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        64,
        2116
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "ac_base_url",
              "value": "https://YOURACCOUNT.api-us1.com",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "sector_tags",
              "value": "[]",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "setup-config",
      "name": "Setup settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        284,
        2116
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.ac_base_url }}/api/3/fields?limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "get-fields",
      "name": "Get existing fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        504,
        2116
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Setup settings').first().json;\nconst existing = ($input.first().json.fields || []).map(f => f.title);\n\nconst wanted = [\n  { title: 'chat_session_id',        type: 'text',     perstag: 'CHAT_SESSION_ID' },\n  { title: 'chat_first_seen_url',    type: 'text',     perstag: 'CHAT_FIRST_SEEN_URL' },\n  { title: 'chat_last_summary',      type: 'textarea', perstag: 'CHAT_LAST_SUMMARY' },\n  { title: 'chat_consent_timestamp', type: 'text',     perstag: 'CHAT_CONSENT_TIMESTAMP' },\n  { title: 'chat_consent_wording',   type: 'textarea', perstag: 'CHAT_CONSENT_WORDING' }\n];\n\nconst missing = wanted.filter(f => !existing.includes(f.title));\n\nif (missing.length === 0) {\n  return [{ json: { skip: true, message: 'All 5 custom fields already exist.' } }];\n}\n\nreturn missing.map(f => ({\n  json: {\n    skip: false,\n    ac_base_url: cfg.ac_base_url,\n    body: { field: { title: f.title, type: f.type, perstag: f.perstag, visible: 1 } }\n  }\n}));"
      },
      "id": "plan-fields",
      "name": "Which fields are missing?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        724,
        2116
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.skip }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-fields-needed",
      "name": "Any fields to create?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        944,
        2116
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.ac_base_url }}/api/3/fields",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "create-fields",
      "name": "Create custom fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1164,
        2016
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Setup settings').first().json.ac_base_url }}/api/3/tags?limit=200",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "get-tags",
      "name": "Get existing tags",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1384,
        2116
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $('Setup settings').first().json;\nconst existing = ($input.first().json.tags || []).map(t => t.tag);\n\nlet sectors = [];\ntry { sectors = JSON.parse(cfg.sector_tags || '[]'); } catch (e) { sectors = []; }\n\nconst wanted = [\n  'source:chat',\n  'interest:pricing','interest:services','interest:integrations','interest:demo',\n  'interest:support','interest:partnership','interest:careers',\n  'intent:hot','intent:warm','intent:browsing',\n  'objection:price','objection:timing','objection:competitor',\n  'objection:technical-fit','objection:authority',\n  'consent:explicit-chat','consent:declined',\n  ...sectors.map(s => 'sector:' + String(s).toLowerCase().trim().replace(/\\s+/g,'-'))\n];\n\nconst missing = wanted.filter(t => !existing.includes(t));\n\nif (missing.length === 0) {\n  return [{ json: { skip: true, message: 'All tags already exist.' } }];\n}\n\nreturn missing.map(t => ({\n  json: {\n    skip: false,\n    ac_base_url: cfg.ac_base_url,\n    body: { tag: { tag: t, tagType: 'contact', description: 'Created by n8nChatUI lead capture recipe' } }\n  }\n}));"
      },
      "id": "plan-tags",
      "name": "Which tags are missing?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1604,
        2116
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c2",
              "leftValue": "={{ $json.skip }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-tags-needed",
      "name": "Any tags to create?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1824,
        2116
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.ac_base_url }}/api/3/tags",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "options": {}
      },
      "id": "create-tags",
      "name": "Create tags",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2044,
        2016
      ]
    },
    {
      "parameters": {
        "url": "={{ $('Setup settings').first().json.ac_base_url }}/api/3/lists?limit=100",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "get-lists",
      "name": "Get your lists",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2264,
        2116
      ]
    },
    {
      "parameters": {
        "jsCode": "const lists = ($input.first().json.lists || []).map(l => ({ id: l.id, name: l.name }));\n\nconst lines = [\n  '=== SETUP COMPLETE ===',\n  '',\n  'Custom fields and tags are in place.',\n  '',\n  'YOUR LISTS — copy the id you want opted-in contacts added to,',\n  'and paste it into list_id in the main workflow CONFIG node:',\n  ''\n];\n\nfor (const l of lists) {\n  lines.push('  id ' + l.id + '  ->  ' + l.name);\n}\n\nif (lists.length === 0) {\n  lines.push('  (no lists found — create one in ActiveCampaign first)');\n}\n\nlines.push('');\nlines.push('Next: open the main workflow, fill in CONFIG, then test in the chat panel.');\n\nreturn [{ json: { summary: lines.join('\\n'), lists } }];"
      },
      "id": "summarise",
      "name": "Setup summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2484,
        2116
      ]
    }
  ],
  "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": "Tool settings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the agent it failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool settings": {
      "main": [
        [
          {
            "node": "Resolve field IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve field IDs": {
      "main": [
        [
          {
            "node": "Map field names to IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map field names to IDs": {
      "main": [
        [
          {
            "node": "Create or update contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or update contact": {
      "main": [
        [
          {
            "node": "Build field values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build field values": {
      "main": [
        [
          {
            "node": "Write custom fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write custom fields": {
      "main": [
        [
          {
            "node": "CONSENT GATE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CONSENT GATE": {
      "main": [
        [
          {
            "node": "Subscribe to list",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build tag list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Subscribe to list": {
      "main": [
        [
          {
            "node": "Build tag list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build tag list": {
      "main": [
        [
          {
            "node": "Find tag ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find tag ID": {
      "main": [
        [
          {
            "node": "Match tag IDs exactly",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match tag IDs exactly": {
      "main": [
        [
          {
            "node": "Apply tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply tags": {
      "main": [
        [
          {
            "node": "Attach summary note",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach summary note": {
      "main": [
        [
          {
            "node": "Tell the agent it worked",
            "type": "main",
            "index": 0
          },
          {
            "node": "Event tracking (disabled)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Click to run setup": {
      "main": [
        [
          {
            "node": "Setup settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Setup settings": {
      "main": [
        [
          {
            "node": "Get existing fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get existing fields": {
      "main": [
        [
          {
            "node": "Which fields are missing?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Which fields are missing?": {
      "main": [
        [
          {
            "node": "Any fields to create?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any fields to create?": {
      "main": [
        [
          {
            "node": "Create custom fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get existing tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create custom fields": {
      "main": [
        [
          {
            "node": "Get existing tags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get existing tags": {
      "main": [
        [
          {
            "node": "Which tags are missing?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Which tags are missing?": {
      "main": [
        [
          {
            "node": "Any tags to create?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any tags to create?": {
      "main": [
        [
          {
            "node": "Create tags",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get your lists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create tags": {
      "main": [
        [
          {
            "node": "Get your lists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get your lists": {
      "main": [
        [
          {
            "node": "Setup summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}