AutomationFlowsAI & RAG › WhatsApp AI Appointment Booking with GPT-4 and Cal.com

WhatsApp AI Appointment Booking with GPT-4 and Cal.com

Original n8n title: Automate Multi-modal Appointment Booking via Whatsapp Using Gpt-4 and Cal.com

ByStéphane Bordas @stephanebordas on n8n.io

This workflow is for healthcare professionals, consultants, coaches, and service businesses who want to completely automate their appointment booking system via WhatsApp — without manual intervention for reservations, availability checks, or cancellation management.

Event trigger★★★★☆ complexityAI-powered26 nodesWhatsAppOpenAIHTTP RequestAgentOpenAI ChatMemory Buffer WindowHTTP Request ToolTool Http Request
AI & RAG Trigger: Event Nodes: 26 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Agent → HTTP Request 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": "9c865c94-70ef-4322-978e-22b28a3d7a2d",
      "name": "Code",
      "type": "n8n-nodes-base.code",
      "position": [
        2384,
        1552
      ],
      "parameters": {
        "jsCode": "/**\n * n8n \u2013 Code node\n * Mode : Run Once for All Items\n * Objectif :\n *   1. Rep\u00e8re **gras** ou *gras*\n *   2. Convertit le contenu en lettres \"Unicode Bold\"\n *   3. Supprime tous les ast\u00e9risques\n */\n\nconst FIELD = 'output'; // \u2190 mets ici le nom exact du champ texte\n\n// --- petite fonction de conversion ASCII \u2192 Unicode gras\nfunction toUnicodeBold(str) {\n  const upperOffset = 0x1D400 - 0x41;      // A \u2192 \ud835\udc00\n  const lowerOffset = 0x1D41A - 0x61;      // a \u2192 \ud835\udc1a\n  const digitOffset = 0x1D7CE - 0x30;      // 0 \u2192 \ud835\udfce\n\n  return str.split('').map(ch => {\n    const code = ch.charCodeAt(0);\n    if (code >= 0x41 && code <= 0x5A) return String.fromCodePoint(code + upperOffset); // A-Z\n    if (code >= 0x61 && code <= 0x7A) return String.fromCodePoint(code + lowerOffset); // a-z\n    if (code >= 0x30 && code <= 0x39) return String.fromCodePoint(code + digitOffset); // 0-9\n    return ch; // ponctuation, espaces\u2026\n  }).join('');\n}\n\nfor (const item of $input.all()) {\n  if (item.json[FIELD]) {\n    item.json[FIELD] = item.json[FIELD]\n      // \u25ba 1. **\u2026**  ou *\u2026*  \u2192 conversion + suppression d\u2019ast\u00e9risques\n      .replace(/\\*{1,2}([^*]+?)\\*{1,2}/gs, (_, txt) => toUnicodeBold(txt))\n\n      // \u25ba 2. S\u2019il reste des ast\u00e9risques isol\u00e9s \u2192 on les retire\n      .replace(/\\*/g, '');\n  }\n}\n\nreturn $input.all();"
      },
      "typeVersion": 2
    },
    {
      "id": "97c32dd0-65a0-4e9b-94dd-03b9a8b75c7b",
      "name": "Split out message1",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -48,
        1552
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "messages"
      },
      "typeVersion": 1
    },
    {
      "id": "ade79aa0-86ca-4742-9177-0f23ac3d87ed",
      "name": "Identify and ReRoute Message Types1",
      "type": "n8n-nodes-base.switch",
      "position": [
        256,
        1536
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Audio Message",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "584171c6-5842-41a4-ae70-b901457d1b43",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{$json.type == 'audio' && Boolean($json.audio)}}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Image Message",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "b12ff3af-0aae-4874-9c16-37f0337bf214",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{$json.type == 'image' && Boolean($json.image)}}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "renameFallbackOutput": "Text Message"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "0fc5684c-cfc4-413a-ab59-045eaa336de5",
      "name": "Send message",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        2656,
        1552
      ],
      "parameters": {
        "textBody": "={{ $json.output }}",
        "operation": "send",
        "phoneNumberId": "={{ $('WhatsApp Trigger').item.json.metadata.phone_number_id }}",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('WhatsApp Trigger').item.json.messages[0].from }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "8c5bb334-0319-4eeb-8a51-f9bc04e96fd5",
      "name": "Audio Transcriber",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1120,
        1360
      ],
      "parameters": {
        "options": {},
        "resource": "audio",
        "operation": "transcribe"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.8
    },
    {
      "id": "90a2ffc0-4ef1-4175-974d-eae721b50dd2",
      "name": "Analyze image",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1120,
        1552
      ],
      "parameters": {
        "text": "Here is an image sent by the user. Describe the image and transcribe any text visible in the image. Put in as much detail as possible. But only and always in French Language.",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {},
        "resource": "image",
        "inputType": "base64",
        "operation": "analyze"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "a7ca13a9-66a6-42b5-af96-7983bc55a059",
      "name": "Get Audio URL",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        592,
        1360
      ],
      "parameters": {
        "resource": "media",
        "operation": "mediaUrlGet",
        "mediaGetId": "={{ $json.audio.id }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ce1f62be-7f03-4401-8b08-59d44eb5d3d9",
      "name": "Get Image URL",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        592,
        1552
      ],
      "parameters": {
        "resource": "media",
        "operation": "mediaUrlGet",
        "mediaGetId": "={{ $json.image.id }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "da5d655b-66bf-4281-934a-93524540372e",
      "name": "Download Image",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        1552
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "f220b256-48d7-4647-941f-1d73bbc0c0af",
      "name": "Download Audio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        864,
        1360
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "whatsAppApi"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "284674ca-6c30-4ceb-8ece-18d8815832d4",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1984,
        1552
      ],
      "parameters": {
        "text": "={{ $json.user_prompt }}",
        "options": {
          "systemMessage": "=# Nutrition Office Assistant\n\n## \ud83d\udcc5 CURRENT DATE AND TIME\n**Today:** {{ DateTime.now().setZone('Europe/Paris').toFormat('EEEE dd MMMM yyyy', {locale: 'fr'}) }}  \n**Time:** {{ DateTime.now().setZone('Europe/Paris').toFormat('HH:mm:ss') }}  \n**ISO Format:** {{ DateTime.now().setZone('Europe/Paris').toFormat('yyyy-MM-dd') }}\n\n## BASIC INFORMATION\n- **Location:** 8 All\u00e9e du Progr\u00e8s, 92170 Vanves  \n- **Hours:** Mon-Fri 08:00-18:00 | Sat 09:00-15:00  \n- **Timezone:** Europe/Paris (currently UTC+1)  \n- **Date display:** DD-MM-YYYY and HHhMM (never technical format)\n\n## LANGUAGE POLICY\n- The assistant must **speak primarily in French at all times**.  \n- The assistant may **switch to English or Spanish** only if the client initiates the conversation in that language.  \n- When switching languages, continue the full conversation in the client\u2019s chosen language.  \n- In all other cases, **default to French** for clarity, professionalism, and consistency.\n\n## CRITICAL RULES\n\n### 1. TIME CONVERSION (INVISIBLE TO CLIENT)\n- **API \u2192 Client:** Add 1h (UTC to Paris) - Example: API says \"12:00Z\" = say \"13h00\"  \n- **Client \u2192 API:** Subtract 1h (Paris to UTC) - Example: client says \"8h30\" = send \"07:30Z\"  \n- **NEVER mention** UTC, conversion, or timezone to the client\n\n### 2. DATE CALCULATIONS\n- **Use the current date** above for all calculations  \n- **Always verify:** if today = Monday, Nov 3, then \u201cTuesday\u201d = Nov 4 (not 5!)  \n- **Count days precisely:** Monday +1 day = Tuesday, Monday +7 days = next Monday\n\n### 3. BOOKING (book_appointment)\n**Single flow:**\n1. Ask: name, email, phone (+33...), reason  \n2. Make ONE summary: \"Je r\u00e9serve le [date] \u00e0 [heure] pour [motif]. Nom : [X]. C\u2019est bon ?\"  \n3. Client confirms \u2192 **Immediately call book_appointment**  \n4. Announce ONLY if successful: \"C\u2019est r\u00e9serv\u00e9 pour le [date] \u00e0 [heure], au cabinet : 8 All\u00e9e du Progr\u00e8s, 92170 Vanves.\"\n\n**FORBIDDEN:**\n- \u274c Saying \u201cit\u2019s booked\u201d without calling the tool  \n- \u274c Making 2 summaries  \n- \u274c Announcing if the tool failed\n\n### 4. CANCELLATION (cancel_booking)\n**Mandatory sequence:**\n1. `find_booking` using email \u2192 get bookingUid  \n2. Filter: keep ONLY future appointments (not past ones)  \n3. If multiple: ask which one  \n4. Summarize and ask for confirmation  \n5. `cancel_booking` using bookingUid  \n6. Announce ONLY if successful  \n\n**FORBIDDEN:**\n- \u274c Calling cancel_booking without find_booking first  \n- \u274c Using an email as bookingUid  \n- \u274c Showing past appointments to the client\n\n### 5. RESCHEDULING (reschedule_booking)\n**Mandatory sequence:**\n1. `find_booking` using email \u2192 get bookingUid  \n2. Filter: keep ONLY future appointments  \n3. If multiple: ask which one  \n4. `check_availability` \u2192 propose 3\u20134 time slots  \n5. Client chooses \u2192 ask for confirmation  \n6. `reschedule_booking` with bookingUid + new UTC slot  \n7. Announce ONLY if successful  \n\n**Format for new slot:**\n- Parameter: `newStartUtc` (not newEventDateTimeUtc)  \n- Format: `YYYY-MM-DDTHH:MM:SSZ` in UTC  \n- Conversion: client says \"14h\" = send \"13:00Z\"\n\n## AVAILABLE TOOLS\n\n**check_availability:** Check available slots  \n**book_appointment:** Create a booking (after client confirmation)  \n**find_booking:** Find existing appointments (MANDATORY before cancel/reschedule)  \n**cancel_booking:** Cancel (with bookingUid from find_booking)  \n**reschedule_booking:** Reschedule (with bookingUid from find_booking)  \n**retrieve_files:** Internal FAQ\n\n## TONE AND STYLE\n- Warm, concise, efficient  \n- No visible technical jargon  \n- Offer 3\u20134 slots maximum  \n- Always mention the address in confirmations  \n- Communicate in **natural, polite French**, unless the client uses another language  \n\n## FINAL REMINDERS\n1. **Current date:** Always use the date above for calculations  \n2. **Time conversion:** Always +1h from API, -1h to API (silently)  \n3. **Past appointments:** Always filter after find_booking  \n4. **Confirmation:** Announce \u201cc\u2019est fait\u201d ONLY after successful tool execution  \n5. **Find-then-Act:** ALWAYS find_booking before cancel/reschedule  \n6. **Default language:** Always French unless the client starts in English or Spanish"
        },
        "promptType": "define"
      },
      "retryOnFail": true,
      "typeVersion": 2.2
    },
    {
      "id": "08886616-2555-4668-9591-c75e609ef83c",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1616,
        1920
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini",
          "cachedResultName": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "3fbb24d7-e5a8-4bd6-8136-b5a4f205183a",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1792,
        1920
      ],
      "parameters": {
        "sessionKey": "={{ $json.from ?? $('Split out message1').item.json.from ?? $('WhatsApp Trigger').item.json.messages[0].from }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 10
      },
      "typeVersion": 1.3
    },
    {
      "id": "1a0f6899-84ab-4e03-b51c-a7ab98b434f7",
      "name": "check_availability",
      "type": "n8n-nodes-base.httpRequestTool",
      "position": [
        1984,
        1920
      ],
      "parameters": {
        "url": "https://api.cal.com/v2/slots",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "eventTypeId"
            },
            {
              "name": "timeZone",
              "value": "Europe/Paris"
            },
            {
              "name": "start",
              "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters2_Value', `YYYY-MM-DD`, 'string') }}"
            },
            {
              "name": "end",
              "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('parameters3_Value', `YYYY-MM-DD`, 'string') }}"
            }
          ]
        },
        "toolDescription": "Use this tool to retrieves the right slot",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "cal-api-version",
              "value": "2024-09-04"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "13c8a972-f126-4630-a6cc-919fd40eade5",
      "name": "book_appointment",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        2176,
        1920
      ],
      "parameters": {
        "url": "https://api.cal.com/v2/bookings",
        "method": "POST",
        "jsonBody": "{\n  \"start\": \"{eventDateTimeUtc}\",\n  \"eventTypeId\": XXXXXXX,\n  \"attendee\": {\n    \"name\": \"{name}\",\n    \"email\": \"{email}\",\n    \"timeZone\": \"Europe/Paris\",\n    \"phoneNumber\": \"{phoneE164}\",\n    \"language\": \"fr\"\n  },\n  \"bookingFieldsResponses\": {\n    \"title\": \"{title}\"\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "toolDescription": "Create a booking for the selected slot.\nInputs: eventTypeId (required), startUtc (ISO 8601 UTC, required), attendeeEmail (required), attendeeName (optional), notes (optional), timeZone (always pass \u201cEurope/Paris\u201d for the attendee).\nBehavior: If the user provides a local time (Europe/Paris), convert it to startUtc before booking. Confirm a short summary with the user (doctor, date/time in Europe/Paris, attendee email) before creating the booking. On conflicts or validation errors, propose the nearest 2\u20133 alternative slots from check_availability.\nOutput: Return the booking object including uid, start, and a human summary.",
        "parametersHeaders": {
          "values": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE",
              "valueProvider": "fieldValue"
            },
            {
              "name": "cal-api-version",
              "value": "2024-08-13",
              "valueProvider": "fieldValue"
            },
            {
              "name": "Content-Type",
              "value": "application/json",
              "valueProvider": "fieldValue"
            }
          ]
        },
        "placeholderDefinitions": {
          "values": [
            {
              "name": "eventDateTimeUtc",
              "type": "string",
              "description": "ex. 2025-08-27T10:30:00Z (UTC avec Z)"
            },
            {
              "name": "name",
              "type": "string",
              "description": "ex : Jean Dupond"
            },
            {
              "name": "email",
              "type": "string",
              "description": "user@example.com"
            },
            {
              "name": "phoneE164",
              "type": "string",
              "description": "ex : +1234567890"
            },
            {
              "name": "title",
              "type": "string",
              "description": "ex : 1\u00e8re consultation"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "2605d75d-a709-41e1-9e98-c3a00bf8f6b6",
      "name": "find_booking",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        2352,
        1920
      ],
      "parameters": {
        "url": "https://api.cal.com/v2/bookings",
        "sendQuery": true,
        "sendHeaders": true,
        "parametersQuery": {
          "values": [
            {
              "name": "attendeeEmail",
              "value": "{email}",
              "valueProvider": "fieldValue"
            }
          ]
        },
        "toolDescription": "Find a user\u2019s booking to obtain its uid.\nInputs: \n- email (required), \n- dateHint (optional, e.g., \"2025-10-17 14:30 Europe/Paris\"), \n- eventTypeId=2957420 (implicit).\n\nBehavior:\n- Call GET /v2/bookings with attendeeEmail=email (and eventTypeId if needed).\n- If dateHint is provided, select the booking whose start is the closest match.\n- If multiple plausible matches remain or no dateHint, list 2\u20133 options (date/time/title in Europe/Paris) and ask the user to choose BEFORE continuing.\n- Return a compact object exposing at least:\n  { bookingUid: <uid>, start: <ISO>, title: <string>, eventTypeId: 2957420 }.\n\nNever return an email as bookingUid. The value bookingUid MUST be the API field \"uid\".",
        "parametersHeaders": {
          "values": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE",
              "valueProvider": "fieldValue"
            },
            {
              "name": "cal-api-version",
              "value": "2024-08-13",
              "valueProvider": "fieldValue"
            },
            {
              "name": "Content-Type",
              "value": "application/json",
              "valueProvider": "fieldValue"
            }
          ]
        },
        "placeholderDefinitions": {
          "values": [
            {
              "name": "email",
              "type": "string",
              "description": "ex: \u201cuser@example.com\u201d"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "6aa95c18-ca31-47cc-b56d-fcb100b00b28",
      "name": "cancel_booking",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        2528,
        1920
      ],
      "parameters": {
        "url": "=https://api.cal.com/v2/bookings/{bookingUid}/cancel",
        "method": "POST",
        "jsonBody": "{\n  \"cancellationReason\": \"{reason}\",\n  \"cancelSubsequentBookings\": {cancelAll}\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "toolDescription": "Cancel a booking by uid.\nInputs: bookingUid (required), reason (string), cancelAll (boolean).\n\nCritical rule:\n- If bookingUid is missing or looks like an email, FIRST call find_booking using the user\u2019s email (and date/time if given). \n- If multiple bookings exist, ask the user to pick the exact one (date/time in Europe/Paris). \n- Only after you have a valid uid, call POST /v2/bookings/{bookingUid}/cancel.\n- On policy/validation issues (e.g., booking in the past), explain briefly and propose alternatives (reschedule or contact support).\n\nOutput: Return the API response and a one-line human confirmation.",
        "parametersHeaders": {
          "values": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE",
              "valueProvider": "fieldValue"
            },
            {
              "name": "cal-api-version",
              "value": "2024-08-13",
              "valueProvider": "fieldValue"
            },
            {
              "name": "Content-Type",
              "value": "application/json",
              "valueProvider": "fieldValue"
            }
          ]
        },
        "placeholderDefinitions": {
          "values": [
            {
              "name": "reason",
              "type": "string",
              "description": "ex: \u201cPatient indisponible\u201d"
            },
            {
              "name": "cancelAll",
              "type": "boolean"
            },
            {
              "name": "bookingUid",
              "type": "string"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "bbd3b15c-715b-4fca-a867-0d00069d8e32",
      "name": "reschedule_booking",
      "type": "@n8n/n8n-nodes-langchain.toolHttpRequest",
      "position": [
        2704,
        1920
      ],
      "parameters": {
        "url": "=https://api.cal.com/v2/bookings/{bookingUid}/reschedule",
        "method": "POST",
        "jsonBody": "{\n  \"start\": \"{newStartUtc}\",\n  \"rescheduledBy\": \"{actorEmail}\",\n  \"reschedulingReason\": \"{reason}\"\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "toolDescription": "Reschedule a booking to a new time.\nInputs: bookingUid (required), newStartUtc (required, ISO 8601 UTC), actorEmail (required), reschedulingReason (optional).\nBehavior: If bookingUid is missing, call find_booking first and confirm the current slot with the user. If the user gives a local time (Europe/Paris), convert to newStartUtc before calling the API. If the requested time is unavailable, show the nearest 2\u20133 alternatives from check_availability and ask the user to pick one. Duration is defined by the event type and doesn\u2019t change on reschedule.\nOutput: Return the updated booking object and a short human summary.\n",
        "parametersHeaders": {
          "values": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE",
              "valueProvider": "fieldValue"
            },
            {
              "name": "cal-api-version",
              "value": "2024-08-13",
              "valueProvider": "fieldValue"
            },
            {
              "name": "Content-Type",
              "value": "application/json",
              "valueProvider": "fieldValue"
            }
          ]
        },
        "placeholderDefinitions": {
          "values": [
            {
              "name": "bookingUid",
              "type": "string"
            },
            {
              "name": "newStartUtc",
              "type": "string",
              "description": "ex. 2025-09-14T13:30:00Z"
            },
            {
              "name": "actorEmail",
              "type": "string"
            },
            {
              "name": "reason",
              "type": "string"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "7e17794b-e460-4e80-a63e-1c2aa0d2fd7a",
      "name": "WhatsApp Trigger",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        -336,
        1552
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "credentials": {
        "whatsAppTriggerApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "22d8b33c-335e-4139-b424-d9120e3ae749",
      "name": "Edit Fields 1",
      "type": "n8n-nodes-base.set",
      "position": [
        1360,
        1776
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5a5d4ec5-61d5-471c-bab7-135528008e6c",
              "name": "user_prompt",
              "type": "string",
              "value": "={{ $json.text.body }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "bb5d4712-8536-4e17-b0f2-12bb33a26340",
      "name": "Edit Fields 2",
      "type": "n8n-nodes-base.set",
      "position": [
        1360,
        1552
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5a5d4ec5-61d5-471c-bab7-135528008e6c",
              "name": "user_prompt",
              "type": "string",
              "value": "={{ $json.content }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "2f41f9fa-07ea-411d-9d64-33f038eb58c6",
      "name": "Edit Fields 3",
      "type": "n8n-nodes-base.set",
      "position": [
        1360,
        1360
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5a5d4ec5-61d5-471c-bab7-135528008e6c",
              "name": "user_prompt",
              "type": "string",
              "value": "={{ $json.text }}"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "9c997b0a-3169-4235-9398-35fb69016801",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        1264
      ],
      "parameters": {
        "color": 4,
        "width": 896,
        "height": 496,
        "content": "# WhatsApp Trigger \u2192 Message Router\n\n**Receives incoming messages** from WhatsApp Business API via webhook.\n\n**Process:**\n- WhatsApp Trigger \u2192 Detects incoming messages\n- Split Out Messages \u2192 Processes each message individually  \n- Identify Message Type \u2192 Routes based on type (Text/Audio/Image)\n\n**Outputs:** 3 branches for different message types"
      },
      "typeVersion": 1
    },
    {
      "id": "9de6353a-2aa9-4835-a4fa-bccd6e63f6b0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        928
      ],
      "parameters": {
        "color": 6,
        "width": 992,
        "height": 1008,
        "content": "# Multi-Modal Processing Pipeline\n\n**Processes all message types** and prepares them for the AI Agent.\n\n### Audio Branch:\n- Get Audio URL \u2192 Download Audio \u2192 Audio Transcriber (Whisper) \u2192 Edit Fields 3\n\n### Image Branch:\n- Get Image URL \u2192 Download Image \u2192 Analyze Image (GPT-4 Vision) \u2192 Edit Fields 2\n\n### Text Branch:\n- Direct text message \u2192 Edit Fields 1\n\n**Output:** All three branches convert their input into `user_prompt` field for the AI Agent"
      },
      "typeVersion": 1
    },
    {
      "id": "0a969050-7193-463a-89c6-3aa11f46c517",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        928
      ],
      "parameters": {
        "color": 5,
        "width": 1280,
        "height": 1136,
        "content": "# AI Agent: Smart Appointment Manager + Response Delivery\n\n**Uses GPT-4** to understand requests in natural language and execute appropriate actions.\n\n### Core Components:\n- **OpenAI Chat Model** - GPT-4 for advanced reasoning\n- **Simple Memory** - Maintains conversation context\n- **AI Agent** - Orchestrates tools based on user intent\n\n### Cal.com Tools (5):\n\u2705 **check_availability** - Checks available time slots\n\u2705 **book_appointment** - Creates new appointments\n\u2705 **find_booking** - Retrieves existing bookings\n\u2705 **cancel_booking** - Cancels appointments by ID\n\u2705 **reschedule_booking** - Changes appointment date/time\n\n### Output Processing:\n- **Code Node** - Converts markdown **bold** to Unicode \ud835\udc1b\ud835\udc28\ud835\udc25\ud835\udc1d for better WhatsApp display\n- **Send Message** - Sends formatted response via WhatsApp Graph API\n\n**The agent automatically selects the right tool based on the conversation and delivers a professional, well-formatted response.**"
      },
      "typeVersion": 1
    },
    {
      "id": "6e57c084-87f1-473f-b7d2-bb16e00f4a25",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        928
      ],
      "parameters": {
        "width": 624,
        "height": 1120,
        "content": "# WhatsApp AI Appointment Agent with Cal.com\n\n### Automate your appointment booking 24/7 via WhatsApp\n\n**What it does:**\n- Understands text, audio, and image messages\n- Checks real-time availability in Cal.com\n- Books, reschedules, and cancels appointments autonomously\n- Responds in natural language\n- Maintains conversation context\n\n**Quick Setup:**\n1. Create Meta App at developers.facebook.com\n2. Add WhatsApp product to your app\n3. Connect credentials (WhatsApp, Cal.com, OpenAI)\n4. Paste webhook URL in Meta Console\n5. Customize AI Agent personality\n6. Test & deploy!\n\n---\n\n## \u2699\ufe0f Configuration Required\n\n### \ud83d\udd10 Credentials:\n- **WhatsApp Business API** - phone_number_id + access_token from Meta\n- **Cal.com API Key** - Get from cal.com/settings/developer\n- **OpenAI API Key** - For GPT-4, Whisper, and Vision\n\n### \ud83d\udcdd Customization:\n- Edit AI Agent **System Message** to define personality and context\n- Configure Cal.com **event types** (duration, pricing, availability)\n- Set **timezone** (default: Europe/Paris)\n- Adjust response **language** in System Message\n\n### \ud83d\ude80 Deployment:\nTest with different message types (text/audio/image) before activating in production.\n\n\ud83d\udcac Need help? Contact: **[LinkedIn](https://www.linkedin.com/in/st%C3%A9phane-bordas-3439b4179/)** "
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "Send message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "find_booking": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Analyze image": {
      "main": [
        [
          {
            "node": "Edit Fields 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields 1": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields 2": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields 3": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Audio URL": {
      "main": [
        [
          {
            "node": "Download Audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Image URL": {
      "main": [
        [
          {
            "node": "Download Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Download Audio": {
      "main": [
        [
          {
            "node": "Audio Transcriber",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image": {
      "main": [
        [
          {
            "node": "Analyze image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cancel_booking": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "WhatsApp Trigger": {
      "main": [
        [
          {
            "node": "Split out message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "book_appointment": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Audio Transcriber": {
      "main": [
        [
          {
            "node": "Edit Fields 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Split out message1": {
      "main": [
        [
          {
            "node": "Identify and ReRoute Message Types1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check_availability": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "reschedule_booking": {
      "ai_tool": [
        [
          {
            "node": "AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Identify and ReRoute Message Types1": {
      "main": [
        [
          {
            "node": "Get Audio URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get Image URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields 1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

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

About this workflow

This workflow is for healthcare professionals, consultants, coaches, and service businesses who want to completely automate their appointment booking system via WhatsApp — without manual intervention for reservations, availability checks, or cancellation management.

Source: https://n8n.io/workflows/11188/ — 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

Transform your salon/service business with this streamlined WhatsApp automation system featuring Claude integration, zero-setup database management, and intelligent conversation handling. Claude MCP I

WhatsApp Trigger, WhatsApp, Redis +11
AI & RAG

This n8n workflow is designed for Facebook Page administrators, social media managers, and community moderators who want to automate comment management on their Facebook Pages. It's perfect for busine

Facebook Graph Api, Agent, HTTP Request +8
AI & RAG

This n8n workflow creates an intelligent WhatsApp customer support bot that can handle text, image, audio, and document messages. The workflow automatically processes incoming messages through differe

HTTP Request, N8N Nodes Rapiwa, Agent Tool +9
AI & RAG

48_WAgentEnhancement. Uses whatsAppTrigger, whatsApp, openAi, httpRequest. Event-driven trigger; 56 nodes.

WhatsApp Trigger, WhatsApp, OpenAI +13
AI & RAG

This workflow creates a complete AI-powered restaurant ordering system through WhatsApp. It receives customer messages, processes multimedia content (text, voice, images, PDFs, location), uses GPT-4 t

OpenAI Chat, Memory Postgres Chat, HTTP Request +6