AutomationFlowsAI & RAG › Book Whatsapp Consultations and Sync Contacts with Airtable and Google Calendar

Book Whatsapp Consultations and Sync Contacts with Airtable and Google Calendar

ByBlaine Holt @fenrirlabsnl on n8n.io

This n8n template automates appointment booking via WhatsApp Flows with real-time calendar availability, AI-powered intent classification, and CRM synchronization. It transforms manual booking conversations into a seamless self-service experience directly within WhatsApp.

Webhook trigger★★★★★ complexityAI-powered41 nodesHTTP RequestOpenAI ChatHTTP Request ToolAirtableAgentGoogle Calendar
AI & RAG Trigger: Webhook Nodes: 41 Complexity: ★★★★★ AI nodes: yes Added:

This workflow corresponds to n8n.io template #12763 — we link there as the canonical source.

This workflow follows the Agent → Airtable recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "nodes": [
    {
      "id": "c46f192b-d4f6-4227-979b-d257151a33d9",
      "name": "GET: Verify Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        80,
        144
      ],
      "parameters": {
        "path": "whatsapp-webhook",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "f70a36e8-e057-42ca-b3cc-a3b3dc868464",
      "name": "Verify Token",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        144
      ],
      "parameters": {
        "jsCode": "// Handle GET verification request from WhatsApp\nconst query = $input.first().json.query || {};\nconst VERIFY_TOKEN = $env.WHATSAPP_VERIFY_TOKEN || 'your_verify_token_here';\n\nif (query['hub.mode'] !== 'subscribe') {\n  throw new Error('Invalid hub.mode');\n}\n\nif (query['hub.verify_token'] !== VERIFY_TOKEN) {\n  throw new Error('Invalid verify token');\n}\n\nreturn {\n  json: {\n    challenge: query['hub.challenge']\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "32acd5a4-c26c-4bf8-9f40-9a49a19dfdac",
      "name": "Return Challenge",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        544,
        144
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "text",
        "responseBody": "={{ $json.challenge }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6283825a-5b66-40d4-94b9-dc9dc24cefb5",
      "name": "Is Regular Message?",
      "type": "n8n-nodes-base.if",
      "position": [
        -128,
        560
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "bde30647-9802-41d4-a346-3355ab24dca6",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isRegularMessage }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "525cce30-ed11-4735-9bc3-536d2143efc1",
      "name": "Return 200 OK (Regular Message)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        784,
        544
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'received' }) }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d5aeb338-cab9-4e24-a216-cabb36213f46",
      "name": "POST: Receive Messages1",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -576,
        560
      ],
      "parameters": {
        "path": "whatsapp-webhook",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "c062dd3a-da07-4f8a-954d-d6672a8f422a",
      "name": "Decrypt WhatsApp Request1",
      "type": "n8n-nodes-base.code",
      "position": [
        -352,
        560
      ],
      "parameters": {
        "jsCode": "const crypto = require('crypto');\n\nconst PRIVATE_KEY = $env.WHATSAPP_PRIVATE_KEY || '';\nconst PASSPHRASE = $env.WHATSAPP_PRIVATE_KEY_PASSPHRASE || '';\n\nlet privateKey = PRIVATE_KEY;\nif (!PRIVATE_KEY.includes('\\n') && PRIVATE_KEY.includes('\\\\n')) {\n  privateKey = PRIVATE_KEY.replace(/\\\\n/g, '\\n');\n}\n\nconst body = $input.first().json.body;\n\n// Check if this is a regular WhatsApp message (not a Flow request)\nif (body?.entry) {\n  const entry = body.entry[0];\n  const changes = entry?.changes?.[0];\n  const value = changes?.value;\n  const messages = value?.messages;\n\n  if (messages && messages.length > 0) {\n    const message = messages[0];\n    const from = message.from;\n    const messageType = message.type;\n\n    // Handle interactive button replies\n    if (messageType === 'interactive' && message.interactive?.type === 'button_reply') {\n      const buttonId = message.interactive.button_reply.id;\n      const buttonTitle = message.interactive.button_reply.title;\n      const parts = buttonId.split('_');\n      const action = parts[0];\n      const recordId = parts.slice(1).join('_');\n\n      return {\n        json: {\n          messageType: 'button_reply',\n          action: action,\n          recordId: recordId,\n          customerPhone: from,\n          buttonTitle: buttonTitle,\n          isRegularMessage: true\n        }\n      };\n    }\n\n    // Handle text messages\n    if (messageType === 'text') {\n      return {\n        json: {\n          messageType: 'text_message',\n          from: from,\n          text: message.text?.body || '',\n          isRegularMessage: true\n        }\n      };\n    }\n\n    // Other message types\n    return {\n      json: {\n        messageType: messageType,\n        from: from,\n        raw: message,\n        isRegularMessage: true\n      }\n    };\n  }\n\n  // Status update or other non-message webhook\n  return {\n    json: {\n      messageType: 'status_update',\n      isRegularMessage: true,\n      raw: body\n    }\n  };\n}\n\n// This is an encrypted Flow request - decrypt it\nfunction decryptRequest(body) {\n  const { encrypted_aes_key, encrypted_flow_data, initial_vector } = body;\n\n  const decryptedAesKey = crypto.privateDecrypt(\n    {\n      key: privateKey,\n      passphrase: PASSPHRASE,\n      oaepHash: 'sha256',\n      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING\n    },\n    Buffer.from(encrypted_aes_key, 'base64')\n  );\n\n  const flowDataBuffer = Buffer.from(encrypted_flow_data, 'base64');\n  const ivBuffer = Buffer.from(initial_vector, 'base64');\n\n  const authTag = flowDataBuffer.subarray(-16);\n  const encryptedData = flowDataBuffer.subarray(0, -16);\n\n  const decipher = crypto.createDecipheriv('aes-128-gcm', decryptedAesKey, ivBuffer);\n  decipher.setAuthTag(authTag);\n\n  const decrypted = Buffer.concat([\n    decipher.update(encryptedData),\n    decipher.final()\n  ]);\n\n  return {\n    decryptedData: JSON.parse(decrypted.toString('utf8')),\n    aesKey: decryptedAesKey,\n    iv: ivBuffer\n  };\n}\n\nfunction encryptResponse(response, aesKey, iv) {\n  const flippedIv = Buffer.alloc(iv.length);\n  for (let i = 0; i < iv.length; i++) {\n    flippedIv[i] = ~iv[i] & 0xff;\n  }\n  const cipher = crypto.createCipheriv('aes-128-gcm', aesKey, flippedIv);\n  const encrypted = Buffer.concat([\n    cipher.update(JSON.stringify(response), 'utf8'),\n    cipher.final(),\n    cipher.getAuthTag()\n  ]);\n  return encrypted.toString('base64');\n}\n\nconst { decryptedData, aesKey, iv } = decryptRequest(body);\n\nif (decryptedData.action === 'ping') {\n  const pingResponse = {\n    version: decryptedData.version,\n    data: { status: 'active' }\n  };\n\n  return {\n    json: {\n      action: 'ping',\n      response: encryptResponse(pingResponse, aesKey, iv),\n      isRegularMessage: false\n    }\n  };\n}\n\nreturn {\n  json: {\n    decrypted: decryptedData,\n    action: decryptedData.action,\n    screen: decryptedData.screen,\n    data: decryptedData.data || {},\n    flow_token: decryptedData.flow_token,\n    aesKeyBase64: aesKey.toString('base64'),\n    ivBase64: iv.toString('base64'),\n    version: decryptedData.version,\n    isRegularMessage: false\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b37944ec-53ee-4548-8b1f-7bc2c99a44e1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -608,
        304
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 480,
        "content": "## Whatsapp Single Entry Point: Webhook\n\nBecause of Meta's Single App to Single Webhook restriction, all messages come through the same webhook (GET / POST). \n\nConditional statements after this are then used to filter what happens"
      },
      "typeVersion": 1
    },
    {
      "id": "bfc3eb34-787e-4959-9402-73cc0d218f0a",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        1136
      ],
      "parameters": {
        "color": 7,
        "width": 3072,
        "height": 1280,
        "content": "## WhatsApp Flow: Booking\n\nFlows are attached to messaging templates and are activated by the WhatsApp User\n\nAt each stage of the WhatsApp Flow, data is being fed to the experience. E.g Services Available, Calendar Availability, & Confirmation. On complete - a booking is made into Airtable and a Calendar Event is made\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d6f2ba59-2038-49ca-8057-1e379baeed25",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 1296,
        "height": 656,
        "content": "## Intelligent Templating Assignment & Responses\n\nWe want to be able to fire off our template messages and flows that make sense to the customers requests. If they want more information, we can send them a link to the services website.\nIf they want to book - then a Calendar Flow, a quote, then a quote flow. "
      },
      "typeVersion": 1
    },
    {
      "id": "b3551661-9a21-412b-a0b0-b6500d74c05b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 304,
        "content": "## Utility: WhatsApp Webhook Check\nRequired for setting up your n8n webhook in Meta"
      },
      "typeVersion": 1
    },
    {
      "id": "9b87e762-c71d-4073-adb5-265a8f262aa1",
      "name": "Return Ping Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        304,
        1312
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "text",
        "responseBody": "={{ $json.response }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "a9bc81f7-d4c5-4351-abf4-42be2a295e82",
      "name": "Handle INIT - Return Consultation Types",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        1472
      ],
      "parameters": {
        "jsCode": "// Handle INIT action - return initial screen data\nconst formatDate = (d) => d.toISOString().split('T')[0];\nconst today = new Date();\nconst maxDate = new Date();\nmaxDate.setDate(today.getDate() + 30);\n\nconst response = {\n  version: $json.version || '3.0',\n  screen: 'SERVICE_SELECTION',\n  data: {\n    consultation_types: [\n      { id: '30_min', title: '30 Minute Call' },\n      { id: '60_min', title: '60 Minute Call' }\n    ],\n    min_date: formatDate(today),\n    max_date: formatDate(maxDate)\n  }\n};\n\nreturn {\n  json: {\n    response,\n    aesKeyBase64: $json.aesKeyBase64,\n    ivBase64: $json.ivBase64\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "df7e496c-140b-49e8-89ca-e4cd68f07929",
      "name": "Handle Confirm Booking",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        2016
      ],
      "parameters": {
        "jsCode": "// Handle confirm booking - return CONFIRMATION screen data\nconst data = $json.data;\nconst aesKeyBase64 = $json.aesKeyBase64;\nconst ivBase64 = $json.ivBase64;\nconst version = $json.version;\n\nconst response = {\n  version: version || '3.0',\n  screen: 'CONFIRMATION',\n  data: {\n    customer_name: data.customer_name,\n    customer_email: data.customer_email || '',\n    consultation_type: data.consultation_type,\n    consultation_type_label: data.consultation_type_label,\n    appointment_date: data.appointment_date,\n    appointment_time: data.appointment_time\n  }\n};\n\nreturn {\n  json: {\n    response,\n    aesKeyBase64,\n    ivBase64\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f4321129-d5bc-4d7b-ae11-6b1e6d7f39d4",
      "name": "Send WhatsApp Confirmation",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2128,
        2224
      ],
      "parameters": {
        "url": "https://graph.facebook.com/v23.0/{{WHATSAPP_PHONE_NUMBER_ID}}/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"messaging_product\": \"whatsapp\",\n  \"to\": \"{{ $('Check Customer Exists1').first().json.flowToken }}\",\n  \"type\": \"text\",\n  \"text\": {\n    \"body\": \"Your consultation has been confirmed!\\n\\n{{ $('Check Customer Exists1').first().json.booking.consultationLabel }}\\nDate: {{ $('Check Customer Exists1').first().json.booking.appointmentDateDisplay }}\\nTime: {{ $('Check Customer Exists1').first().json.booking.appointmentTimeDisplay }}\\n\\nWe look forward to speaking with you!\"\n  }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "269f5e8b-ab1b-49cc-b6b1-e4615e79b74a",
      "name": "Merge Paths",
      "type": "n8n-nodes-base.code",
      "position": [
        2480,
        1824
      ],
      "parameters": {
        "jsCode": "// Merge all paths to encryption node\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "5c9e17c9-00e5-4d04-82ee-6adaed550ff4",
      "name": "Encrypt Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2688,
        1824
      ],
      "parameters": {
        "jsCode": "// WhatsApp Flow Encryption - AES-128-GCM\nconst crypto = require('crypto');\n\nfunction encryptResponse(response, aesKeyBase64, ivBase64) {\n  const aesKey = Buffer.from(aesKeyBase64, 'base64');\n  const iv = Buffer.from(ivBase64, 'base64');\n  \n  const flippedIv = Buffer.alloc(iv.length);\n  for (let i = 0; i < iv.length; i++) {\n    flippedIv[i] = ~iv[i] & 0xff;\n  }\n  \n  const cipher = crypto.createCipheriv('aes-128-gcm', aesKey, flippedIv);\n  \n  const responseStr = JSON.stringify(response);\n  const encrypted = Buffer.concat([\n    cipher.update(responseStr, 'utf8'),\n    cipher.final(),\n    cipher.getAuthTag()\n  ]);\n  \n  return encrypted.toString('base64');\n}\n\nconst { response, aesKeyBase64, ivBase64 } = $json;\nconst encryptedResponse = encryptResponse(response, aesKeyBase64, ivBase64);\n\nreturn {\n  json: {\n    encryptedResponse\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "dd67e257-f900-4f75-a0a3-61e8b04cbad2",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2912,
        1824
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/plain"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.encryptedResponse }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "439fa7ab-c609-4d15-9fed-943e59c5e0d9",
      "name": "Switch on Action",
      "type": "n8n-nodes-base.switch",
      "position": [
        80,
        1328
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "PING",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "993ed1e9-64a4-427a-b8fc-5256edde1946",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "ping"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "INIT",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "007df454-eab1-4b7a-9e1d-e82781e8e9a7",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "INIT"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "SERVICE_SELECTION",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "46e256b4-e275-43a4-9f6d-d33c338d463a",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "data_exchange"
                  },
                  {
                    "id": "64878584-3e5b-40a1-a0bf-ee11cf51f416",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.screen }}",
                    "rightValue": "SERVICE_SELECTION"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "DATE_REFRESH",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "1a7e63d4-55c8-4ebb-b305-432c69198888",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "data_exchange"
                  },
                  {
                    "id": "664fd262-0803-42ef-96f7-02b6ab477460",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.screen }}",
                    "rightValue": "DATE_TIME_SELECTION"
                  },
                  {
                    "id": "880d3796-67fb-4ea7-860e-ef27172ee346",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.data.action_type }}",
                    "rightValue": "refresh_slots"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "CONFIRM_BOOKING",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "926d9d18-05bc-4156-861e-d3960ee07dca",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "data_exchange"
                  },
                  {
                    "id": "73939400-1292-4b36-b681-1d1c8aa0486a",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.screen }}",
                    "rightValue": "DATE_TIME_SELECTION"
                  },
                  {
                    "id": "72b44982-b940-4b53-a1c7-f1766b9958d5",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.data.action_type }}",
                    "rightValue": "confirm_booking"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "COMPLETE_BOOKING",
              "conditions": {
                "options": {
                  "version": 1,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "ea4498c4-fb40-492a-8703-6bec94c1fde9",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "data_exchange"
                  },
                  {
                    "id": "b1234567-1234-1234-1234-123456789abc",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.screen }}",
                    "rightValue": "CONFIRMATION"
                  },
                  {
                    "id": "c1234567-1234-1234-1234-123456789def",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.data.action_type }}",
                    "rightValue": "complete_booking"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "3e908ab3-eb23-4134-936e-85d4b14cae94",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "position": [
        144,
        528
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "INTERACTIVE",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "b2fd458a-376d-4b87-817e-a92151d3b3ff",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.messageType }}",
                    "rightValue": "interactive"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "STATUS_UPDATE",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "903cf5bf-4180-40da-a7eb-6dd58b37a1b1",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.messageType }}",
                    "rightValue": "status_updated"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "TEXT_MESSAGE",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "bd24999f-353e-40b9-b64c-b6e6c04ac51e",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.messageType }}",
                    "rightValue": "text_message"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "95e15ae0-f4c2-489b-887d-3f2f41c73e11",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        352,
        832
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {},
        "builtInTools": {}
      },
      "typeVersion": 1.3
    },
    {
      "id": "52f72a66-1e70-4545-87b4-03f230148e3b",
      "name": "whatsapp_consult_template",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        704,
        832
      ],
      "parameters": {
        "url": "https://graph.facebook.com/v23.0/{{WHATSAPP_PHONE_NUMBER_ID}}/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"messaging_product\": \"whatsapp\",\n  \"to\": \"{{ $('Is Regular Message?').item.json.from }}\",\n  \"type\": \"template\",\n  \"template\": {\n    \"name\": \"personal_consultation_booking\",\n    \"language\": {\n      \"code\": \"en\"\n    },\n    \"components\": [\n      {\n        \"type\": \"button\",\n        \"sub_type\": \"flow\",\n        \"index\": \"0\",\n        \"parameters\": [\n          {\n            \"type\": \"action\",\n            \"action\": {\n              \"flow_token\": \"{{ $json.from }}\"\n            }\n          }\n        ]\n      }\n    ]\n  }\n}\n",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "toolDescription": "whatsapp_consult_template - use when a user's intent is to book an appointment"
      },
      "typeVersion": 4.3
    },
    {
      "id": "38e2ebc5-be9c-4a8e-ae25-d922ae1660f2",
      "name": "whatsapp_message_tool",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        528,
        832
      ],
      "parameters": {
        "url": "https://graph.facebook.com/v23.0/{{WHATSAPP_PHONE_NUMBER_ID}}/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n    \"messaging_product\": \"whatsapp\",    \n    \"recipient_type\": \"individual\",\n    \"to\": \"{{ $('Is Regular Message?').item.json.from }}\",\n    \"type\": \"text\",\n    \"text\": {\n        \"preview_url\": false,\n        \"body\": \"{{ $fromAI('JSON', ``, 'json') }}\"\n    }\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "toolDescription": "**whatsapp_message**\nUsage: When the user asks a questions, or you require more information to understand their intent. You can reply"
      },
      "typeVersion": 4.3
    },
    {
      "id": "9833334d-fd52-4f0e-ab53-cb16e20463f8",
      "name": "Search Existing Customer",
      "type": "n8n-nodes-base.airtable",
      "notes": "UPDATED: Now searches by customer_email instead of phone_number",
      "onError": "continueRegularOutput",
      "position": [
        416,
        2224
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "url",
          "value": "=https://airtable.com/{{AIRTABLE_BASE_ID}}",
          "__regex": "https://airtable.com/([a-zA-Z0-9]{2,})"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{{AIRTABLE_CUSTOMERS_TABLE_ID}}",
          "cachedResultUrl": "https://airtable.com/{{AIRTABLE_BASE_ID}}/{{AIRTABLE_CUSTOMERS_TABLE_ID}}",
          "cachedResultName": "Customers"
        },
        "options": {},
        "operation": "search",
        "authentication": "airtableOAuth2Api",
        "filterByFormula": "={customer_email}='{{ $json.booking.customerEmail }}'"
      },
      "typeVersion": 2.1,
      "alwaysOutputData": true
    },
    {
      "id": "dc192778-333b-42ec-8726-55a05427efa8",
      "name": "Lookup Service",
      "type": "n8n-nodes-base.airtable",
      "notes": "NEW: Lookup service record by service_key (30_min or 60_min) to get record ID for linking",
      "position": [
        1088,
        2224
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "url",
          "value": "=https://airtable.com/{{AIRTABLE_BASE_ID}}",
          "__regex": "https://airtable.com/([a-zA-Z0-9]{2,})"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{{AIRTABLE_SERVICES_TABLE_ID}}",
          "cachedResultUrl": "https://airtable.com/{{AIRTABLE_BASE_ID}}/{{AIRTABLE_SERVICES_TABLE_ID}}",
          "cachedResultName": "Services"
        },
        "options": {
          "fields": [
            "service_name"
          ]
        },
        "operation": "search",
        "authentication": "airtableOAuth2Api",
        "filterByFormula": "={service_key}='{{ $('Check Customer Exists1').first().json.booking.serviceKey }}'"
      },
      "typeVersion": 2.1
    },
    {
      "id": "0b9e6c1e-0106-41dc-b1c8-d0a53492b81b",
      "name": "Prepare Booking Data1",
      "type": "n8n-nodes-base.code",
      "notes": "UPDATED: Added serviceKey, eventDate, eventTime fields for new schema",
      "position": [
        208,
        2224
      ],
      "parameters": {
        "jsCode": "// Prepare booking data for Airtable and Calendar\nconst data = $json.data;\n\n// Parse YYYY-MM-DD date string\nconst appointmentDate = new Date(data.appointment_date + 'T00:00:00');\nconst [hours, minutes] = data.appointment_time.split(':').map(Number);\n\nappointmentDate.setHours(hours, minutes, 0, 0);\n\nconst duration = data.consultation_type === '60_min' ? 60 : 30;\nconst endTime = new Date(appointmentDate);\nendTime.setMinutes(endTime.getMinutes() + duration);\n\nconst dateStr = appointmentDate.toLocaleDateString('en-US', {\n  weekday: 'long',\n  year: 'numeric',\n  month: 'long',\n  day: 'numeric'\n});\n\nconst timeStr = appointmentDate.toLocaleTimeString('en-US', {\n  hour: 'numeric',\n  minute: '2-digit',\n  hour12: true\n});\n\nreturn {\n  json: {\n    ...$json,\n    booking: {\n      customerName: data.customer_name,\n      customerEmail: data.customer_email || '',\n      consultationType: data.consultation_type,\n      consultationLabel: data.consultation_type_label,\n      serviceKey: data.consultation_type,\n      eventDate: data.appointment_date,\n      eventTime: data.appointment_time,\n      appointmentDateTime: appointmentDate.toISOString(),\n      appointmentEndTime: endTime.toISOString(),\n      appointmentDateDisplay: dateStr,\n      appointmentTimeDisplay: timeStr,\n      duration: duration,\n      smsReminder: data.sms_reminder || false,\n      emailReminder: data.email_reminder || false\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "b4b78f14-467e-4c8f-9a04-a24066931bd1",
      "name": "Create Booking in Airtable1",
      "type": "n8n-nodes-base.airtable",
      "notes": "UPDATED: customer_email (text), service_type (linked), event_date, event_time, booking_status, flow_token. Removed reminder fields.",
      "position": [
        1312,
        2224
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "url",
          "value": "=https://airtable.com/{{AIRTABLE_BASE_ID}}",
          "__regex": "https://airtable.com/([a-zA-Z0-9]{2,})"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{{AIRTABLE_BOOKINGS_TABLE_ID}}",
          "cachedResultUrl": "https://airtable.com/{{AIRTABLE_BASE_ID}}/{{AIRTABLE_BOOKINGS_TABLE_ID}}",
          "cachedResultName": "Bookings"
        },
        "columns": {
          "value": {
            "created_at": "={{ $now }}",
            "event_date": "={{ $('Prepare Booking Data1').item.json.decrypted.data.appointment_date }}",
            "event_time": "={{ $('Prepare Booking Data1').item.json.decrypted.data.appointment_time }}",
            "booking_status": "Pending",
            "customer_email": "={{ $('Upsert Customer in Airtable').item.json.fields.customer_email }}"
          },
          "schema": [
            {
              "id": "booking_id",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "booking_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "customer_email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "service_type",
              "type": "array",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "service_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "calendar_event_id",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "calendar_event_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_date",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "event_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_time",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "event_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "booking_status",
              "type": "options",
              "display": true,
              "options": [
                {
                  "name": "Pending",
                  "value": "Pending"
                },
                {
                  "name": "Confirmed",
                  "value": "Confirmed"
                },
                {
                  "name": "Cancelled",
                  "value": "Cancelled"
                }
              ],
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "booking_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "special_requests",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "special_requests",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        },
        "operation": "create",
        "authentication": "airtableOAuth2Api"
      },
      "typeVersion": 2.1
    },
    {
      "id": "f199a9f1-f6ea-477b-bdd9-61a83a979837",
      "name": "Update Booking with Calendar ID1",
      "type": "n8n-nodes-base.airtable",
      "notes": "UPDATED: status -> booking_status, now sets to 'Confirmed' after calendar event created",
      "position": [
        1808,
        2224
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "url",
          "value": "=https://airtable.com/{{AIRTABLE_BASE_ID}}",
          "__regex": "https://airtable.com/([a-zA-Z0-9]{2,})"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{{AIRTABLE_BOOKINGS_TABLE_ID}}",
          "cachedResultUrl": "https://airtable.com/{{AIRTABLE_BASE_ID}}/{{AIRTABLE_BOOKINGS_TABLE_ID}}",
          "cachedResultName": "Bookings"
        },
        "columns": {
          "value": {
            "id": "={{ $('Create Booking in Airtable1').item.json.id }}",
            "booking_status": "Confirmed",
            "calendar_event_id": "={{ $json.id }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "booking_id",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "booking_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_email",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "customer_email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "service_type",
              "type": "array",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "service_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "calendar_event_id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "calendar_event_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_date",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "event_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "event_time",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "event_time",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "booking_status",
              "type": "options",
              "display": true,
              "options": [
                {
                  "name": "Pending",
                  "value": "Pending"
                },
                {
                  "name": "Confirmed",
                  "value": "Confirmed"
                },
                {
                  "name": "Cancelled",
                  "value": "Cancelled"
                }
              ],
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "booking_status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "special_requests",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "special_requests",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "authentication": "airtableOAuth2Api"
      },
      "typeVersion": 2.1
    },
    {
      "id": "62c6c4dd-ab5e-4da9-986b-9625d1d9f4af",
      "name": "Upsert Customer in Airtable",
      "type": "n8n-nodes-base.airtable",
      "notes": "UPDATED: Field names - customer_name, customer_email, customer_phone_number, date_created, last_interaction",
      "position": [
        880,
        2224
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "url",
          "value": "=https://airtable.com/{{AIRTABLE_BASE_ID}}"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{{AIRTABLE_CUSTOMERS_TABLE_ID}}",
          "cachedResultUrl": "https://airtable.com/{{AIRTABLE_BASE_ID}}/{{AIRTABLE_CUSTOMERS_TABLE_ID}}",
          "cachedResultName": "Customers"
        },
        "columns": {
          "value": {
            "id": "={{ $json.existingCustomerId }}",
            "date_created": "={{ $now }}",
            "customer_name": "={{ $json.booking.customerName }}",
            "customer_email": "={{ $json.booking.customerEmail }}",
            "last_interaction": "={{ $now }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "customer_id",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "customer_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "customer_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "customer_email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "customer_phone_number",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "customer_phone_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "date_created",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "date_created",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_interaction",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "last_interaction",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Service Requests",
              "type": "array",
              "display": true,
              "removed": true,
              "readOnly": false,
              "required": false,
              "displayName": "Service Requests",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        },
        "operation": "upsert",
        "authentication": "airtableOAuth2Api"
      },
      "typeVersion": 2.1
    },
    {
      "id": "70235408-3e69-4bc3-b3c2-3e5c6a492dce",
      "name": "Google Calendar Events (Date)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        544,
        1840
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ encodeURIComponent($json.calendarId) }}/events",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "timeMin",
              "value": "={{ $json.timeMin }}"
            },
            {
              "name": "timeMax",
              "value": "={{ $json.timeMax }}"
            },
            {
              "name": "singleEvents",
              "value": "true"
            },
            {
              "name": "orderBy",
              "value": "startTime"
            },
            {
              "name": "maxResults",
              "value": "50"
            }
          ]
        },
        "nodeCredentialType": "googleCalendarOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "32cff87f-af74-4cae-97e9-acc2830f3cad",
      "name": "Calculate Slots for Date",
      "type": "n8n-nodes-base.code",
      "position": [
        768,
        1840
      ],
      "parameters": {
        "jsCode": "// Calculate slots for selected date using Events API (including pending/tentative)\nconst events = ($json.items || []).filter(e => e.status !== 'cancelled');\nconst prev = $('Prepare Date Refresh Request1').first().json;\nconst consultationType = prev.consultationType;\nconst selectedDateStr = prev.selectedDate;\nconst customerName = prev.customerName;\nconst customerEmail = prev.customerEmail;\nconst aesKeyBase64 = prev.aesKeyBase64;\nconst ivBase64 = prev.ivBase64;\nconst version = prev.version;\n\nconst slotDuration = consultationType === '60_min' ? 60 : 30;\nconst consultationLabel = consultationType === '60_min' ? '60 Minute Call' : '30 Minute Call';\n\nconst businessStart = 9;\nconst businessEnd = 17;\n\n// Convert events to busy time blocks\nconst busyTimes = events.map(event => {\n  // Handle all-day events\n  if (event.start.date) {\n    return {\n      start: new Date(event.start.date + 'T00:00:00'),\n      end: new Date(event.end.date + 'T00:00:00')\n    };\n  }\n  return {\n    start: new Date(event.start.dateTime),\n    end: new Date(event.end.dateTime)\n  };\n});\n\nconst selectedDate = new Date(selectedDateStr + 'T00:00:00');\nconst now = new Date();\nconst availableSlots = [];\n\nconst dayOfWeek = selectedDate.getDay();\nif (dayOfWeek !== 0 && dayOfWeek !== 6) {\n  for (let hour = businessStart; hour < businessEnd; hour++) {\n    for (let minute = 0; minute < 60; minute += 30) {\n      const slotEnd = hour + (minute + slotDuration) / 60;\n      if (slotEnd > businessEnd) continue;\n\n      const slotStart = new Date(selectedDate);\n      slotStart.setHours(hour, minute, 0, 0);\n\n      // Skip past slots\n      if (slotStart <= now) continue;\n\n      const slotEndTime = new Date(slotStart);\n      slotEndTime.setMinutes(slotEndTime.getMinutes() + slotDuration);\n\n      // Check for conflicts with any event (confirmed, tentative, etc.)\n      const isConflict = busyTimes.some(busy => {\n        return (slotStart < busy.end && slotEndTime > busy.start);\n      });\n\n      if (!isConflict) {\n        const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;\n        const displayHour = hour > 12 ? hour - 12 : hour;\n        const ampm = hour >= 12 ? 'PM' : 'AM';\n        const displayMin = minute.toString().padStart(2, '0');\n\n        availableSlots.push({\n          id: timeStr,\n          title: `${displayHour}:${displayMin} ${ampm}`\n        });\n      }\n    }\n  }\n}\n\nconst formatDate = (d) => d.toISOString().split('T')[0];\nconst today = new Date();\nconst minDate = formatDate(today);\nconst maxDate = formatDate(new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000));\n\nconst response = {\n  version: version || '3.0',\n  screen: 'DATE_TIME_SELECTION',\n  data: {\n    customer_name: customerName,\n    customer_email: customerEmail,\n    consultation_type: consultationType,\n    consultation_type_label: consultationLabel,\n    min_date: minDate,\n    max_date: maxDate,\n    available_slots: availableSlots\n  }\n};\n\nreturn {\n  json: {\n    response,\n    aesKeyBase64,\n    ivBase64\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2ea36a0e-ea3d-4e30-afdf-0ba322f985ea",
      "name": "Prepare Date Refresh Request1",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        1840
      ],
      "parameters": {
        "jsCode": "// Prepare Google Calendar Events request for specific selected date\nconst selectedDate = $json.data.selected_date;\n\nconst date = new Date(selectedDate + 'T00:00:00');\n\nconst dayStart = new Date(date);\ndayStart.setHours(0, 0, 0, 0);\n\nconst dayEnd = new Date(date);\ndayEnd.setHours(23, 59, 59, 999);\n\nconst calendarId = $env.GOOGLE_CALENDAR_ID || 'primary';\n\nreturn {\n  json: {\n    ...$json,\n    selectedDate: selectedDate,\n    calendarId: calendarId,\n    timeMin: dayStart.toISOString(),\n    timeMax: dayEnd.toISOString(),\n    consultationType: $json.data.consultation_type,\n    customerName: $json.data.customer_name,\n    customerEmail: $json.data.customer_email || ''\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c821c29b-7144-49ae-8a40-528c6ac9d561",
      "name": "Check Customer Exists1",
      "type": "n8n-nodes-base.code",
      "notes": "UPDATED: Field names changed to customer_name, customer_email, customer_phone_number, date_created, last_interaction",
      "position": [
        640,
        2224
      ],
      "parameters": {
        "jsCode": "// Check if customer exists and prepare upsert data\nconst existingCustomers = $input.all();\nconst booking = $('Prepare Booking Data1').first().json.booking;\nconst flowToken = $('Prepare Booking Data1').first().json.flow_token;\nconst aesKeyBase64 = $('Prepare Booking Data1').first().json.aesKeyBase64;\nconst ivBase64 = $('Prepare Booking Data1').first().json.ivBase64;\nconst version = $('Prepare Booking Data1').first().json.version;\n\nconst customerExists = existingCustomers.length > 0 && existingCustomers[0].json.id;\nconst existingCustomerId = customerExists ? existingCustomers[0].json.id : null;\n\nreturn {\n  json: {\n    customerExists,\n    existingCustomerId,\n    booking,\n    flowToken,\n    aesKeyBase64,\n    ivBase64,\n    version,\n    customerData: {\n      customer_name: booking.customerName,\n      customer_email: booking.customerEmail,\n      customer_phone_number: flowToken,\n      date_created: new Date().toISOString().split('T')[0],\n      last_interaction: new Date().toISOString().split('T')[0]\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "955a58cf-8d98-4eda-83e2-f8d94c88a872",
      "name": "Prepare Success Response1",
      "type": "n8n-nodes-base.code",
      "notes": "FIXED: Gets encryption keys from Check Customer Exists1 instead of Search Existing Customer",
      "position": [
        2496,
        2208
      ],
      "parameters": {
        "jsCode": "// Prepare success response\nconst aesKeyBase64 = $('Check Customer Exists1').first().json.aesKeyBase64;\nconst ivBase64 = $('Check Customer Exists1').first().json.ivBase64;\nconst version = $('Check Customer Exists1').first().json.version;\n\nconst response = {\n  version: version || '3.0',\n  screen: 'SUCCESS',\n  data: {\n    extension_message_response: {\n      params: {\n        flow_token: $('Check Customer Exists1').first().json.flowToken,\n        booking_confirmed: true\n      }\n    }\n  }\n};\n\nreturn {\n  json: {\n    response,\n    aesKeyBase64,\n    ivBase64\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "2df0aba0-0fb1-42ce-b71b-e3df3c7496f6",
      "name": "Whatsapp Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        352,
        640
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "options": {
          "systemMessage": "=## Role & Goal\n\nYou are a helpful WhatsApp Assistant for [Your Company Name]. Your goal is to respond to customers and determine the best response or WhatsApp template to serve them. \n\n## Task\n\nYou are given a text message from a user. You will then aim to understand how to help them. Based on their response, you will serve up a WhatsApp template or a message\n\n## Tools\n\nYou have access to the following tools:\n\n**whatsapp_consult_template**\nAction: Sends a whatsapp template that allows a user to book an appointment\nUsage: When the user's intent is to book a meeting\n\n**whatsapp_message**\nAction: Sends a text message back to the user\nUsage: When the user asks a questions, or you require more information to understand their intent\n\n## Examples\n- If a user messages \"Hello\", you respond with a greeting and asking how can [Your Company Name] help\n- If a user's intent is to book a consult, you will use the 'whatsapp_consult_template' too\n\n## Rules\n- MUST NOT do anything other than provide 'text' to the whatsapp_message tool. It should not be an object just pure text.\n\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "8cbbb696-c1fa-4ae7-af16-725b43f6591e",
      "name": "Prepare Calendar Request",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        1680
      ],
      "parameters": {
        "jsCode": "// Prepare Google Calendar Events request for today only\n// Configure timezone to match your calendar\nconst TIMEZONE = 'Europe/Amsterdam';\n\nconst now = new Date();\n\n// Set timeMax to end of current day\nconst endOfDay = new Date(now);\nendOfDay.setHours(23, 59, 59, 999);\n\nconst calendarId = $env.GOOGLE_CALENDAR_ID || 'primary';\n\nreturn {\n  json: {\n    ...$json,\n    calendarId: calendarId,\n    timezone: TIMEZONE,\n    timeMin: now.toISOString(),\n    timeMax: endOfDay.toISOString(),\n    consultationType: $json.data.consultation_type,\n    customerName: $json.data.customer_name,\n    customerEmail: $json.data.customer_email || ''\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "04b1b51e-26d8-4c3f-b61b-2af8301dcce2",
      "name": "Google Calendar Events",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        544,
        1680
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ encodeURIComponent($json.calendarId) }}/events",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "timeMin",
              "value": "={{ $json.timeMin }}"
            },
            {
              "name": "timeMax",
              "value": "={{ $json.timeMax }}"
            },
            {
              "name": "timeZone",
              "value": "={{ $json.timezone }}"
            },
            {
              "name": "singleEvents",
              "value": "true"
            },
            {
              "name": "orderBy",
              "value": "startTime"
            },
            {
              "name": "maxResults",
              "value": "250"
            }
          ]
        },
        "nodeCredentialType": "googleCalendarOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "3711b099-f69e-4fd6-956a-cd62436a68e5",
      "name": "Calculate Available Slots",
      "type": "n8n-nodes-base.code",
      "position": [
        768,
        1680
      ],
      "parameters": {
        "jsCode": "// Calculate available slots using calendar timezone\nconst TIMEZONE = $('Prepare Calendar Request').first().json.timezone;\nconst BUSINESS_START = 9;\nconst BUSINESS_END = 17;\n\nconst events = ($json.items || []).filter(e => e.status !== 'cancelled');\nconst consultationType = $('Prepare Calendar Request').first().json.consultationType;\nconst customerName = $('Prepare Calendar Request').first().json.customerName;\nconst customerEmail = $('Prepare Calendar Request').first().json.customerEmail;\nconst aesKeyBase64 = $('Prepare Calendar Request').first().json.aesKeyBase64;\nconst ivBase64 = $('Prepare Calendar Request').first().json.ivBase64;\nconst version = $('Prepare Calendar Request').first().json.version;\n\nconst slotDuration = consultationType === '60_min' ? 60 : 30;\nconst consultationLabel = consultationType === '60_min' ? '60 Minute Call' : '30 Minute Call';\n\n// Get current time in the calendar's timezone\nconst nowUtc = new Date();\nconst tzFormatter = new Intl.DateTimeFormat('en-CA', {\n  timeZone: TIMEZONE,\n  year: 'numeric', month: '2-digit', day: '2-digit',\
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This n8n template automates appointment booking via WhatsApp Flows with real-time calendar availability, AI-powered intent classification, and CRM synchronization. It transforms manual booking conversations into a seamless self-service experience directly within WhatsApp.

Source: https://n8n.io/workflows/12763/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Automate candidate evaluation from CV submission to interview booking. Perfect for HR teams and recruiters.

Airtable, HTTP Request, Information Extractor +6
AI & RAG

What if AI didn't just write content—but actually thought about how to write it? This n8n workflow revolutionizes content creation by deploying multiple specialized AI agents that handle every aspect

Tool Http Request, Anthropic Chat, Airtable +7
AI & RAG

This workflow automatically processes new free-trial / lead sign-ups in real time: Catches a webhook from any source (Webflow form, Intercom, custom agent, etc.) Filters out personal / disposable / .e

Output Parser Structured, Agent, HTTP Request +7
AI & RAG

|Overview |Sample| |-|-| |This template is the first of its kind: it automatically generates both the caption and the image for your Instagram posts by analysing your existing feed, with zero spreadsh

Airtable, HTTP Request, Agent +4
AI & RAG

This workflow transforms WhatsApp into a powerful personal AI using n8n + Green-API. Send text or voice messages — the assistant understands intent and handles daily tasks automatically. 💰 Expense & i

Tool Calculator, Google Sheets Tool, OpenAI Chat +10