{
  "name": "Book Calendly meetings from a website chatbot using OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Book Calendly meetings from a website chatbot using OpenAI\n\nFor consultants and agencies who use Calendly and want visitors to book a call from the chat. The agent 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 times from Calendly.\n4. The book slot tool creates the invitee for the confirmed time. 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 Calendly 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 paid Calendly plan with a personal access token, event type URIs and SMTP credentials.\n\n### Customization tips\nChange the event types, days ahead, timezone or the notification method. Add a location if your event type needs one.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "87b9a3a9-1110-4991-bee6-ce200ce585bd",
      "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": "8a95d299-574e-43d4-a468-840a99789452",
      "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": "d8a207e7-5a65-4a4d-aa66-5988772acc10",
      "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": "0309f9e4-b252-4d5c-8d68-42ae801ae7e1",
      "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": "3926dd4b-e688-408a-870d-af166e4dd17d",
      "name": "Section 4: Tool: book_slot (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "calendly-booking-chatbot",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "calendly-booking-chatbot-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Company",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "event_type_new_project",
              "value": "https://api.calendly.com/event_types/PASTE_URI_HERE",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "event_type_support",
              "value": "https://api.calendly.com/event_types/PASTE_URI_HERE",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "days_ahead",
              "value": 7,
              "type": "number"
            },
            {
              "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": "c8",
              "name": "webhook_secret",
              "value": "CHANGE_ME_TO_A_RANDOM_STRING",
              "type": "string"
            },
            {
              "id": "c9",
              "name": "message",
              "value": "={{ $json.body.chatInput || $json.body.message || '' }}",
              "type": "string"
            },
            {
              "id": "c10",
              "name": "session_id",
              "value": "={{ $json.body.sessionId || 'no-session' }}",
              "type": "string"
            },
            {
              "id": "c11",
              "name": "page_url",
              "value": "={{ $json.body.metadata?.page_url || '' }}",
              "type": "string"
            },
            {
              "id": "c12",
              "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, 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 Calendly event type. Read-only, safe to call whenever you need to see what's available. Pass event_type as the full event type URI from CONFIG.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_CHECK_AVAILABILITY_WORKFLOW_ID_HERE",
          "cachedResultName": "check_availability (Calendly)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"event_type\": \"https://api.calendly.com/event_types/XXXXXXXX\",\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 Calendly. 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 (Calendly)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"event_type\": \"https://api.calendly.com/event_types/XXXXXXXX\",\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": "const 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": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "days_ahead",
              "value": 7,
              "type": "number"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        504,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// Calendly's availability endpoint has a hard cap on how far out you can\n// query in a single call (roughly a week in most accounts) - days_ahead\n// beyond that will need multiple calls, not attempted here for v1.\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: new Date(now.getTime() + 60000).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.calendly.com/event_type_available_times",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "event_type",
              "value": "={{ $json.event_type }}"
            },
            {
              "name": "start_time",
              "value": "={{ $json.start_iso }}"
            },
            {
              "name": "end_time",
              "value": "={{ $json.end_iso }}"
            }
          ]
        },
        "options": {}
      },
      "id": "call-availability-api",
      "name": "Call Calendly availability API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// [TBC] - the exact envelope key for this endpoint's response wasn't fully\n// confirmed at build time. Calendly's list-style GETs typically wrap results\n// as { collection: [...] }; each item has start_time, status, invitees_remaining.\n// Verify against a live call before publishing and adjust the key below if needed.\nconst body = $input.first().json;\nconst cfg = $('Build date range').first().json;\n\nconst raw = body.collection || body.data || (Array.isArray(body) ? body : []);\nconst available = raw.filter(s => !s.status || s.status === 'available');\nconst limited = available.slice(0, 8).map(s => s.start_time);\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": "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  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 doesn't 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": "timezone",
              "value": "Europe/London",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "days_ahead",
              "value": 7,
              "type": "number"
            },
            {
              "id": "s3",
              "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.calendly.com/invitees",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ event_type: $json.event_type, start_time: $json.start_time, invitee: { name: $json.name, email: $json.email, timezone: $json.timezone } }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        }
      },
      "id": "call-invitee-api",
      "name": "Call Calendly Create Event Invitee",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        944,
        1716
      ]
    },
    {
      "parameters": {
        "jsCode": "// fullResponse is on: real payload is under .body, real status under .statusCode.\n// [TBC] - Calendly's exact error envelope for this endpoint wasn't confirmed at\n// build time (docs list 400/401/403/404/500 as possible codes but not bodies).\n// Any non-2xx is treated as a reason to recover rather than guessing at a\n// specific 'conflict' shape - same deliberately permissive approach used for\n// Cal.com, and for the same reason: better to over-recover than surface a raw\n// error to the visitor.\nconst full = $input.first().json;\nconst statusCode = full.statusCode || 200;\nconst body = full.body || full;\n\nreturn [{ json: {\n  ...$('Tool settings (book slot)').first().json,\n  booking_conflict: statusCode >= 400,\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: new Date(now.getTime() + 60000).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.calendly.com/event_type_available_times",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "event_type",
              "value": "={{ $json.event_type }}"
            },
            {
              "name": "start_time",
              "value": "={{ $json.start_iso }}"
            },
            {
              "name": "end_time",
              "value": "={{ $json.end_iso }}"
            }
          ]
        },
        "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 raw = body.collection || body.data || (Array.isArray(body) ? body : []);\nconst available = raw.filter(s => !s.status || s.status === 'available');\nconst limited = available.slice(0, 5).map(s => s.start_time);\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": "// [TBC] - the exact shape of a successful Create Event Invitee response\n// (the 'resource' object) wasn't fully enumerated at build time. This assumes\n// a top-level 'resource' key per Calendly's usual single-resource pattern.\nconst cfg = $input.first().json;\nconst booking = cfg.booking_result;\nconst resource = booking?.resource || booking;\n\nreturn [{ json: {\n  ...cfg,\n  booking_uri: resource?.uri || '',\n  confirmed_start: resource?.event?.start_time || 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 }}",
        "text": "=A new booking was made through the chatbot.\n\nName: {{ $json.name }}\nEmail: {{ $json.email }}\nTime: {{ $json.confirmed_start }}\nPage: {{ $json.page_url }}\nBooking URI: {{ $json.booking_uri }}",
        "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 Calendly availability API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Calendly availability 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 Calendly Create Event Invitee",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Calendly Create Event Invitee": {
      "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": []
}