{
  "name": "Book Cal.com meetings from a website chatbot using OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Book Cal.com meetings from a website chatbot using OpenAI\n\nFor consultants and agencies who want visitors to book a call from the chat. The agent picks the right event type, offers real slots and books only after a clear confirmation.\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 chooses between your new project and support event types.\n3. The check availability tool reads open slots from Cal.com.\n4. The book slot tool books the confirmed slot. If it fails, it offers fresh alternatives.\n5. It emails your team when a booking is made. The confirmation rule lives in the prompt, not in code.\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 Cal.com 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 Cal.com API key with your username and event type slugs, and SMTP credentials.\n\n### Customization tips\nChange the event types, days ahead, timezone or the notification method.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "d9b3726a-acef-42e1-8ab7-4f5380a04e67",
      "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": "c6869219-220a-4954-a6bd-08b8a98daf07",
      "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": 512,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        528
      ],
      "id": "7eaf42d4-9e05-4531-89e1-103d110e583f",
      "name": "Section 2: Answer with the AI agent"
    },
    {
      "parameters": {
        "content": "## 3. Tool: check_availability (copy to its own workflow)\nCopy these nodes into a new workflow and save it. Then select that workflow in the check_availability tool node.",
        "width": 1328,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1136
      ],
      "id": "1c16d451-43a0-4755-b4ab-14059f6b0109",
      "name": "Section 3: Tool: check_availability (copy to its own workflow)"
    },
    {
      "parameters": {
        "content": "## 4. Tool: book_slot (copy to its own workflow)\nCopy these nodes into a new workflow and save it. Then select that workflow in the book_slot tool node.",
        "width": 2208,
        "height": 672,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1504
      ],
      "id": "79472df6-7410-45d2-9e2a-697bbaf5a10f",
      "name": "Section 4: Tool: book_slot (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "cal-booking-chatbot",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "cal-booking-chatbot-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Company",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "event_type_new_project",
              "value": "new-project",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "event_type_support",
              "value": "support-call",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "c7",
              "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, when the conversation genuinely points there, help them book time on the calendar.\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.\n- Never invent pricing, availability, or capability.\n\nBOOKING - which event type\n- Use event type \"{{ $json.event_type_new_project }}\" for visitors who sound like a new prospect (new project, getting started, considering working with you).\n- Use event type \"{{ $json.event_type_support }}\" for visitors who sound like an existing customer needing help.\n- If it genuinely is not clear which fits, ask one short question rather than guessing.\n\nCHECKING AVAILABILITY\n- Call check_availability with the chosen event type once you know which one fits. It is safe to call this freely - it only reads, it does not book anything.\n- Present a small number of options in plain language, in {{ $json.timezone }}. Do not dump a long raw list.\n\nBOOKING - confirmation gate, follow this exactly\n- Never call book_slot on a time you merely offered. Only call it after the visitor has clearly confirmed one specific time.\n- \"Yes\", \"sounds good\", or silence in response to a vague question is NOT a confirmation of a specific slot. If there is any doubt which time they mean, ask them to confirm the exact one before booking.\n- You need a name and an email before calling book_slot. Ask for whichever you don't have yet, one at a time.\n- If book_slot reports the slot is no longer available, offer the alternatives it returns and ask the visitor to pick again. Do not apologise excessively, just offer the next options plainly.\n\nAlways pass session_id \"{{ $json.session_id }}\" and page_url \"{{ $json.page_url }}\" when calling either tool.\n\nTONE\n- Short. Plain. No exclamation marks. Do not say \"Great question!\".\n- If you do not know something, say so."
        }
      },
      "id": "agent",
      "name": "Answer questions and book meetings",
      "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": "check_availability",
        "description": "Look up open time slots for a Cal.com event type. Read-only, safe to call whenever you need to see what's available. Pass event_type as either the new-project or support event type slug from CONFIG.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_CHECK_AVAILABILITY_WORKFLOW_ID_HERE",
          "cachedResultName": "check_availability (Cal.com)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"event_type\": \"new-project\",\n  \"session_id\": \"\",\n  \"page_url\": \"\"\n}"
      },
      "id": "tool-check-availability",
      "name": "check_availability",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 1.2,
      "position": [
        544,
        880
      ]
    },
    {
      "parameters": {
        "name": "book_slot",
        "description": "Book a confirmed time slot in Cal.com. Only call this after the visitor has explicitly confirmed one specific time, and you have their name and email. If the slot has gone, this returns fresh alternatives instead of booking.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_BOOK_SLOT_WORKFLOW_ID_HERE",
          "cachedResultName": "book_slot (Cal.com)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"event_type\": \"new-project\",\n  \"start_time\": \"2026-09-10T14:00:00Z\",\n  \"name\": \"Sam\",\n  \"email\": \"sam@example.com\",\n  \"session_id\": \"\",\n  \"page_url\": \"\"\n}"
      },
      "id": "tool-book-slot",
      "name": "book_slot",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 1.2,
      "position": [
        784,
        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,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// Normalise whatever the agent sent.\nconst q = $input.first().json.query ?? $input.first().json;\nconst d = typeof q === 'string' ? JSON.parse(q) : q;\n\nreturn [{ json: {\n  event_type: d.event_type || '',\n  session_id: d.session_id || '',\n  page_url: d.page_url || ''\n} }];"
      },
      "id": "parse-input",
      "name": "Parse input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        284,
        1248
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "cal_username",
              "value": "your-cal-username",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "days_ahead",
              "value": 7,
              "type": "number"
            },
            {
              "id": "s4",
              "name": "cal_api_version",
              "value": "2024-09-04",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        504,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build the start/end date range for the slots lookup.\nconst cfg = $input.first().json;\nconst now = new Date();\nconst end = new Date(now.getTime() + (Number(cfg.days_ahead) || 7) * 24 * 60 * 60 * 1000);\n\nreturn [{ json: {\n  ...cfg,\n  start_iso: now.toISOString(),\n  end_iso: end.toISOString()\n} }];"
      },
      "id": "build-date-range",
      "name": "Build date range",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        724,
        1248
      ]
    },
    {
      "parameters": {
        "url": "https://api.cal.com/v2/slots",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "cal-api-version",
              "value": "={{ $json.cal_api_version }}"
            }
          ]
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "eventTypeSlug",
              "value": "={{ $json.event_type }}"
            },
            {
              "name": "username",
              "value": "={{ $json.cal_username }}"
            },
            {
              "name": "start",
              "value": "={{ $json.start_iso }}"
            },
            {
              "name": "end",
              "value": "={{ $json.end_iso }}"
            },
            {
              "name": "timeZone",
              "value": "={{ $json.timezone }}"
            }
          ]
        },
        "options": {}
      },
      "id": "call-slots-api",
      "name": "Call Cal.com slots API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// Trim the raw slots response to a short, plain list for the agent.\n// Cal.com v2 slots response: { status, data: { \"YYYY-MM-DD\": [{ start: \"...\" }, ...] } }.\n// Also accepts `time` for older shapes.\nconst body = $input.first().json;\nconst cfg = $('Build date range').first().json;\nconst byDate = (body.data || body || {});\n\nconst flat = [];\nfor (const [date, slots] of Object.entries(byDate)) {\n  for (const slot of (slots || [])) {\n    flat.push(slot.time || slot.start || slot);\n  }\n}\n\nconst limited = flat.slice(0, 8);\n\nif (limited.length === 0) {\n  return [{ json: { response: 'No availability found for this event type in the next ' + cfg.days_ahead + ' days.' } }];\n}\n\nconst lines = limited.map(t => '- ' + t);\n\nreturn [{ json: {\n  response: 'Available times (' + cfg.timezone + '):\\n' + lines.join('\\n'),\n  raw_slots: limited\n} }];"
      },
      "id": "format-for-agent",
      "name": "Format for the agent",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1164,
        1248
      ]
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "sub-trigger",
      "name": "When called by the agent (book slot)",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        64,
        1856
      ]
    },
    {
      "parameters": {
        "jsCode": "// Normalise whatever the agent sent 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  name: d.name || '',\n  event_type: d.event_type || '',\n  start_time: (d.start_time && !isNaN(new Date(d.start_time))) ? new Date(d.start_time).toISOString() : (d.start_time || ''),\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,
        1856
      ]
    },
    {
      "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,
        1856
      ]
    },
    {
      "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 booked.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "invalid-response",
      "name": "Tell the agent it failed",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        2016
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "cal_username",
              "value": "your-cal-username",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "days_ahead",
              "value": 7,
              "type": "number"
            },
            {
              "id": "s4",
              "name": "cal_api_version",
              "value": "2024-08-13",
              "type": "string"
            },
            {
              "id": "s5",
              "name": "handoff_email",
              "value": "you@example.com",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings (book slot)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        724,
        1716
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.cal.com/v2/bookings",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "cal-api-version",
              "value": "={{ $json.cal_api_version }}"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ start: $json.start_time, eventTypeSlug: $json.event_type, username: $json.cal_username, attendee: { name: $json.name, email: $json.email, timeZone: $json.timezone } }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        }
      },
      "id": "call-bookings-api",
      "name": "Call Cal.com bookings API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        1716
      ]
    },
    {
      "parameters": {
        "jsCode": "// fullResponse is on, so the real payload is under .body and the real\n// status code is under .statusCode - both were silently unreachable before.\n//\n// Confirmed against Cal.com's own docs and issue tracker: the error envelope is\n// { status: 'error', error: { code, message } }, and a slot conflict specifically\n// surfaces as message 'no_available_users_found_error'. Checked for that first,\n// but Cal.com's own bug tracker shows this exact error firing as a false positive\n// on genuinely open slots (their issue #20406, roughly half the time in some\n// reports) - so ANY error response still falls back to the same recovery path\n// rather than trying to be clever about which errors 'really' mean a conflict.\nconst full = $input.first().json;\nconst statusCode = full.statusCode || 200;\nconst body = full.body || full;\n\nconst errorMessage = body?.error?.message || '';\nconst isKnownConflict = errorMessage === 'no_available_users_found_error';\nconst hasError = body?.status === 'error' || !!body?.error;\nconst isFailure = statusCode >= 400 || hasError;\n\nreturn [{ json: {\n  ...$('Tool settings (book slot)').first().json,\n  booking_conflict: isFailure,\n  known_conflict_type: isKnownConflict,\n  booking_result: body\n} }];"
      },
      "id": "check-conflict",
      "name": "Booking conflict?",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1164,
        1716
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "bc1",
              "leftValue": "={{ $json.booking_conflict }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-conflict",
      "name": "Was it a conflict?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1384,
        1716
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $input.first().json;\nconst now = new Date();\nconst end = new Date(now.getTime() + (Number(cfg.days_ahead) || 7) * 24 * 60 * 60 * 1000);\n\nreturn [{ json: {\n  ...cfg,\n  start_iso: now.toISOString(),\n  end_iso: end.toISOString()\n} }];"
      },
      "id": "build-date-range-retry",
      "name": "Build date range (retry)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1604,
        1616
      ]
    },
    {
      "parameters": {
        "url": "https://api.cal.com/v2/slots",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "cal-api-version",
              "value": "={{ $json.cal_api_version }}"
            }
          ]
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "eventTypeSlug",
              "value": "={{ $json.event_type }}"
            },
            {
              "name": "username",
              "value": "={{ $json.cal_username }}"
            },
            {
              "name": "start",
              "value": "={{ $json.start_iso }}"
            },
            {
              "name": "end",
              "value": "={{ $json.end_iso }}"
            },
            {
              "name": "timeZone",
              "value": "={{ $json.timezone }}"
            }
          ]
        },
        "options": {}
      },
      "id": "recheck-availability",
      "name": "Re-check availability",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1824,
        1616
      ]
    },
    {
      "parameters": {
        "jsCode": "const body = $input.first().json;\nconst byDate = (body.data || body || {});\n\nconst flat = [];\nfor (const [date, slots] of Object.entries(byDate)) {\n  for (const slot of (slots || [])) {\n    flat.push(slot.time || slot.start || slot);\n  }\n}\n\nconst limited = flat.slice(0, 5);\nconst lines = limited.map(t => '- ' + t);\n\nreturn [{ json: {\n  response: 'That time is not available right now. Here is what is open:\\n' + (lines.length ? lines.join('\\n') : '(nothing else open in the next few days — ask the visitor if a later window works)')\n} }];"
      },
      "id": "format-conflict-alternatives",
      "name": "Format conflict alternatives",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2044,
        1616
      ]
    },
    {
      "parameters": {
        "jsCode": "const cfg = $input.first().json;\nconst booking = cfg.booking_result;\nconst contactId = booking?.data?.id || booking?.id || '';\n\nreturn [{ json: {\n  ...cfg,\n  booking_id: contactId,\n  confirmed_start: booking?.data?.start || booking?.start || cfg.start_time\n} }];"
      },
      "id": "extract-booking-details",
      "name": "Extract booking details",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1604,
        1816
      ]
    },
    {
      "parameters": {
        "fromEmail": "bookings@example.com",
        "toEmail": "={{ $json.handoff_email }}",
        "subject": "=New booking: {{ $json.name }} ({{ $json.event_type }})",
        "text": "=A new booking was made through the chatbot.\n\nName: {{ $json.name }}\nEmail: {{ $json.email }}\nEvent type: {{ $json.event_type }}\nTime: {{ $json.confirmed_start }}\nPage: {{ $json.page_url }}\nBooking ID: {{ $json.booking_id }}",
        "options": {}
      },
      "id": "notify-by-email",
      "onError": "continueRegularOutput",
      "name": "Notify by email",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1824,
        1816
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "response",
              "value": "=Booked. {{ $('Extract booking details').first().json.name }} is confirmed for {{ $('Extract booking details').first().json.confirmed_start }}. Tell them to check their email for the calendar invite.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "success-response",
      "name": "Tell the agent it worked",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2044,
        1816
      ]
    }
  ],
  "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 book meetings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject unknown requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer questions and book meetings": {
      "main": [
        [
          {
            "node": "Respond to widget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4.1 mini model": {
      "ai_languageModel": [
        [
          {
            "node": "Answer questions and book meetings",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Conversation memory": {
      "ai_memory": [
        [
          {
            "node": "Answer questions and book meetings",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "check_availability": {
      "ai_tool": [
        [
          {
            "node": "Answer questions and book meetings",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "book_slot": {
      "ai_tool": [
        [
          {
            "node": "Answer questions and book meetings",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "When called by the agent": {
      "main": [
        [
          {
            "node": "Parse input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse input": {
      "main": [
        [
          {
            "node": "Tool settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool settings": {
      "main": [
        [
          {
            "node": "Build date range",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build date range": {
      "main": [
        [
          {
            "node": "Call Cal.com slots API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Cal.com slots API": {
      "main": [
        [
          {
            "node": "Format for the agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When called by the agent (book slot)": {
      "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 (book slot)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Tell the agent it failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool settings (book slot)": {
      "main": [
        [
          {
            "node": "Call Cal.com bookings API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Cal.com bookings API": {
      "main": [
        [
          {
            "node": "Booking conflict?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Booking conflict?": {
      "main": [
        [
          {
            "node": "Was it a conflict?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Was it a conflict?": {
      "main": [
        [
          {
            "node": "Build date range (retry)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract booking details",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build date range (retry)": {
      "main": [
        [
          {
            "node": "Re-check availability",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Re-check availability": {
      "main": [
        [
          {
            "node": "Format conflict alternatives",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract booking details": {
      "main": [
        [
          {
            "node": "Notify by email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify by email": {
      "main": [
        [
          {
            "node": "Tell the agent it worked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}