{
  "name": "Build a button menu website chatbot without AI using webhooks",
  "nodes": [
    {
      "parameters": {
        "content": "## Build a button menu website chatbot without AI using webhooks\n\nFor site owners who want a simple guided chatbot with no AI costs. Visitors click buttons and get scripted answers that you define in one settings node.\n\n### How it works\n1. A webhook receives each button click from your chat widget and checks a shared secret.\n2. A Code node matches the clicked label against every option in the menu tree, falling back to the start menu.\n3. The reply returns the message plus the next buttons as follow-up prompts.\n\n### Setup\n- [ ] Edit the `menu_tree` in **Chatbot settings** and set a random `webhook_secret`.\n- [ ] Test with a POST to the webhook Test URL (the n8n chat panel needs a Chat Trigger).\n- [ ] Activate, then paste the production webhook URL into your widget with the same secret in `metadata.webhook_secret`.\n\n### Requirements\nn8n and a chat widget that posts `chatInput` and shows `followUpPrompts` (built for n8n Chat UI).\n\n### Customization tips\nAdd menu nodes and buttons in `menu_tree`. Every button label must be unique across the whole tree.",
        "width": 480,
        "height": 992
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "b81fc8af-0bb3-4c33-9913-1a83b3135c4b",
      "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": "ef92121a-5b9a-41ad-b48b-eab18a8df980",
      "name": "Section 1: Receive and verify the message"
    },
    {
      "parameters": {
        "content": "## 2. Match the button and reply\nFinds the clicked label in the menu tree, then returns the message and the next buttons.",
        "width": 704,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        528
      ],
      "id": "aedbc6ef-3f11-4683-97ef-34060dfb5ebf",
      "name": "Section 2: Match the button and reply"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "chip-menu-chatbot",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "chip-menu-chatbot-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c2",
              "name": "webhook_secret",
              "value": "CHANGE_ME_TO_A_RANDOM_STRING",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "menu_tree",
              "value": "{\n  \"start\": {\n    \"message\": \"Hi! What can I help with?\",\n    \"options\": [\n      { \"label\": \"Pricing\", \"next\": \"pricing\" },\n      { \"label\": \"Services\", \"next\": \"services\" },\n      { \"label\": \"Talk to a person\", \"next\": \"handoff\" }\n    ]\n  },\n  \"pricing\": {\n    \"message\": \"Our plans start at [PRICE]/month. [ONE MORE SENTENCE OF DETAIL].\",\n    \"options\": [{ \"label\": \"Back to menu\", \"next\": \"start\" }]\n  },\n  \"services\": {\n    \"message\": \"We offer [SERVICE ONE], [SERVICE TWO], and [SERVICE THREE].\",\n    \"options\": [{ \"label\": \"Back to menu\", \"next\": \"start\" }]\n  },\n  \"handoff\": {\n    \"message\": \"You can reach us directly at [CONTACT EMAIL OR PHONE].\",\n    \"options\": [{ \"label\": \"Back to menu\", \"next\": \"start\" }]\n  }\n}",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "message",
              "value": "={{ $json.body.chatInput || $json.body.message || '' }}",
              "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": {
        "jsCode": "// Find which node the visitor's message matches, by comparing it against\n// every option label in the whole tree (not just the current node's own\n// options). Every label in menu_tree MUST be unique across the entire tree -\n// reusing a label anywhere else silently makes the LAST definition win, since\n// nothing here tracks which node the visitor is actually on. Falls back to\n// the start node if nothing matches.\nconst cfg = $input.first().json;\nconst tree = JSON.parse(cfg.menu_tree);\nconst message = String(cfg.message || '').trim().toLowerCase();\n\nlet nextId = 'start';\n\nfor (const nodeId of Object.keys(tree)) {\n  const node = tree[nodeId];\n  for (const opt of (node.options || [])) {\n    if (opt.label.trim().toLowerCase() === message) {\n      nextId = opt.next;\n    }\n  }\n}\n\nif (!tree[nextId]) {\n  nextId = 'start';\n}\n\nreturn [{ json: { ...cfg, tree, matched_node: nextId } }];"
      },
      "id": "find-node",
      "name": "Find current node",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        64,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build the response: the matched node's message, plus its buttons as followUpPrompts.\nconst { tree, matched_node } = $input.first().json;\nconst node = tree[matched_node];\n\nconst followUpPrompts = (node.options || []).map(o => o.label);\n\nreturn [{ json: {\n  output: node.message,\n  followUpPrompts\n} }];"
      },
      "id": "build-response",
      "name": "Build response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        304,
        640
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ output: $json.output, followUpPrompts: $json.followUpPrompts }) }}",
        "options": {}
      },
      "id": "respond",
      "name": "Respond to widget",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        544,
        640
      ]
    }
  ],
  "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": "Find current node",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject unknown requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find current node": {
      "main": [
        [
          {
            "node": "Build response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build response": {
      "main": [
        [
          {
            "node": "Respond to widget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}