AutomationFlowsAI & RAG › Handle Whatsapp Gym Bookings and Satisfaction Surveys with Claude and Google…

Handle Whatsapp Gym Bookings and Satisfaction Surveys with Claude and Google…

Original n8n title: Handle Whatsapp Gym Bookings and Satisfaction Surveys with Claude and Google Sheets

ByOscar @oscarbekai on n8n.io

A complete WhatsApp AI chatbot that handles class bookings, cancellations, FAQ responses, schedule lookups, location queries, waitlist management, booking reminders, and staff notifications — all through WhatsApp in English, Arabic, or French. Receives WhatsApp messages via…

Webhook trigger★★★★★ complexityAI-powered58 nodesHTTP RequestOpenAIGoogle SheetsTool WorkflowAgentAnthropic Chat
AI & RAG Trigger: Webhook Nodes: 58 Complexity: ★★★★★ AI nodes: yes Added:

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

This workflow follows the Agent → Google Sheets 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
{
  "id": "1VZ9ZVfErajEjLqd",
  "name": "Gymbot Ai Agent Template",
  "tags": [],
  "nodes": [
    {
      "id": "50a4234d-8322-43dc-8614-04d0a4cb1b23",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        16,
        416
      ],
      "parameters": {
        "path": "gymbot/conversation",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "83f91755-6754-49b8-a0a0-933f9697a1e5",
      "name": "Normalize Message",
      "type": "n8n-nodes-base.code",
      "position": [
        368,
        416
      ],
      "parameters": {
        "jsCode": "// NORMALIZE MESSAGE - HANDLE DOUBLE NESTING\nfunction toWhatsAddr(v) {\n  if (!v) return '';\n  const s = String(v).trim();\n  if (s.includes('@c.us')) return s;\n  const digits = s.replace(/\\D/g, '');\n  return digits ? digits + '@c.us' : '';\n}\n\nfunction detectLang(t) {\n  if (!t || typeof t !== 'string') return 'en';\n  const s = t.trim();\n  const arChars = s.match(/[\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF\\uFB50-\\uFDFF\\uFE70-\\uFEFF]/g) || [];\n  const arScript = arChars.length >= 3 || (s.length && arChars.length / s.length >= 0.2);\n  if (arScript) return 'ar';\n  const frWords = /\\b(bonjour|salut|merci|svp|comment|pourquoi|oui|non)\\b/i.test(s);\n  if (frWords) return 'fr';\n  return 'en';\n}\n\n// Handle nested webhook data - Ultramsg wraps data in body.data\nlet webhookData = $('Webhook').first().json.body || $('Webhook').first().json;\n\n// If $json has body property, unwrap it\nif ($json.body && typeof $json.body === 'object') {\n  webhookData = $json.body;\n}\n\n// If webhookData has data property, unwrap that too\nif (webhookData.data && typeof webhookData.data === 'object') {\n  webhookData = webhookData.data;\n}\n\n// Filter outgoing messages\nif (webhookData.type === 'sent' || webhookData.fromMe === true) {\n  return [];\n}\n\n// Block group chats\nif (webhookData.from?.includes('@g.us')) {\n  return [];\n}\n\nconst fromRaw = webhookData.from || '';\nconst messageText = String(webhookData.body || '');\n\n// \u2705 FIXED: Extract user name with multiple fallbacks\nconst userName = webhookData.pushname || \n                 webhookData.name || \n                 webhookData.notifyName ||\n                 webhookData.author ||\n                 'WhatsApp User';\n\nlet voice = null;\nif (webhookData.type === 'ptt' || webhookData.type === 'audio') {\n  voice = {\n    downloadUrl: webhookData.media || '',\n    caption: webhookData.caption || ''\n  };\n}\n\nconst senderWhats = toWhatsAddr(fromRaw);\nconst botWhats = toWhatsAddr(webhookData.to || '');\nconst sessionKey = senderWhats.replace('@c.us', '').replace('whatsapp:', '').replace('+', '').replace(/\\D/g, '');\nconst langCode = detectLang(messageText);\n\nconsole.log('=== NORMALIZE MESSAGE ===');\nconsole.log('User Name:', userName);\nconsole.log('Phone:', sessionKey);\nconsole.log('Message:', messageText);\n\nreturn [{\n  json: {\n    profileName: userName,  // Keep for compatibility\n    userName: userName,      // \u2705 ADD THIS - for booking tool\n    text: messageText,\n    senderWhats,\n    botWhats,\n    sessionKey,\n    phone: sessionKey,\n    sessionId: sessionKey,\n    isVoice: !!voice,\n    voice,\n    lang: { code: langCode }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "eade377e-4a26-49aa-af62-b60fe43c3a8f",
      "name": "IF: Is Voice?",
      "type": "n8n-nodes-base.if",
      "position": [
        992,
        944
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "169d2c3f-ae6e-44cb-bbbd-7cde89d9d343",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.isVoice }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "cc36d0eb-7887-49c4-a5a3-ba90cf836ff6",
      "name": "Download Audio",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1232,
        800
      ],
      "parameters": {
        "url": "={{ $json.voice.downloadUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4
    },
    {
      "id": "3b876c5e-90ac-43f6-9d5c-bbd13333ab5f",
      "name": "Transcribe (Whisper)",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1440,
        800
      ],
      "parameters": {
        "options": {
          "language": "={{ $('Normalize Message').item.json.lang.code }}"
        },
        "resource": "audio",
        "operation": "transcribe"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0d70cea4-223c-46e8-9260-cbecd8c89a3c",
      "name": "Merge Transcription",
      "type": "n8n-nodes-base.code",
      "position": [
        1648,
        800
      ],
      "parameters": {
        "jsCode": "const normalizeData = $node[\"Normalize Message\"].json;\nconst transcription = $json.text || '';\n\nconsole.log('=== MERGE TRANSCRIPTION ===');\nconsole.log('Transcribed text:', transcription);\n\nreturn [{\n  json: {\n    ...normalizeData,\n    text: transcription,\n    isVoiceTranscribed: true\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a6ca89fd-c5ea-45bd-9c86-d4fbcacc6333",
      "name": "Lookup Session",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1904,
        960
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.sessionKey.replace('@c.us', '').replace('whatsapp:', '') }}",
              "lookupColumn": "Phone"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CWcULthIhOq8kcaCVoVWJTkbKBSJ6aMum-jSt2_fQUc/edit#gid=0",
          "cachedResultName": "Session_State"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "c1425d86-32ab-42c3-bb1e-894deb05d03b",
      "name": "Check Session Expiry",
      "type": "n8n-nodes-base.code",
      "position": [
        2096,
        960
      ],
      "parameters": {
        "jsCode": "const SESSION_TIMEOUT_HOURS = 24;\n\n// Get session data from Lookup (may be empty for new users)\nconst sessionData = $input.first()?.json || {};\n\n// Get current message data from Normalize Message node\nconst currentMessageData = $node[\"Normalize Message\"].json;\n\n// Check if voice was transcribed\nlet messageText = currentMessageData.text || '';\nif (currentMessageData.isVoice && $node[\"Merge Transcription\"]) {\n  const mergeData = $node[\"Merge Transcription\"].json;\n  if (mergeData && mergeData.text) {\n    messageText = mergeData.text;\n  }\n}\n\n// Check if session exists and is not expired\nconst now = new Date();\nlet isSessionValid = false;\nlet existingSession = null;\n\nif (sessionData.Phone) {\n  const updatedAt = sessionData.UpdatedAt ? new Date(sessionData.UpdatedAt) : null;\n  \n  if (updatedAt) {\n    const hoursSinceUpdate = (now - updatedAt) / (1000 * 60 * 60);\n    isSessionValid = hoursSinceUpdate < SESSION_TIMEOUT_HOURS;\n  }\n  \n  if (isSessionValid) {\n    try {\n      existingSession = sessionData.State_JSON ? JSON.parse(sessionData.State_JSON) : {};\n    } catch (e) {\n      existingSession = {};\n    }\n  }\n}\n\n// Read surveySubmitted regardless of session validity\nconst surveySubmitted = sessionData.SurveySubmitted || '';\n\n// Get branch and session info\nlet branch = (isSessionValid && sessionData.Branch) ? sessionData.Branch : null;\nconst lang = (isSessionValid && existingSession) ? existingSession.lang : (currentMessageData.lang?.code || 'en');\nconst lastMentionedClass = (isSessionValid && existingSession) ? existingSession.lastMentionedClass : null;\nconst lastMentionedTime = (isSessionValid && existingSession) ? \n  existingSession.lastMentionedTime : null;\nconst pendingBookings = (isSessionValid && existingSession) ? (existingSession.pendingBookings || []) : [];\nconst latestQuestion = (isSessionValid && sessionData.Latest_Question) ? sessionData.Latest_Question : '';\n\n// ===================================================================\n// EXACT BRANCH DETECTION\n// Add new branches here when needed\n// ===================================================================\nconst cfg = $('Read Config').first().json;\nconst validBranches = [cfg?.branch_1 || 'rabieh', cfg?.branch_2 || 'bikfaya'];\n\nconst currentMessageLower = messageText.toLowerCase().trim();\n\nconst mentionedBranch = validBranches.find(b =>\n  currentMessageLower === b ||\n  currentMessageLower === b + ' branch' ||\n  currentMessageLower === 'the ' + b ||\n  currentMessageLower === 'the ' + b + ' branch'\n);\n\nif (mentionedBranch) {\n  branch = mentionedBranch;\n  console.log('\u2705 Branch detected (exact):', branch);\n} else if (!branch) {\n  const inlineBranch = validBranches.find(b => currentMessageLower.includes(b));\n  if (inlineBranch) {\n    branch = inlineBranch;\n    console.log('\u2705 Branch detected (inline):', branch);\n  }\n}\n\nconsole.log('=== CHECK SESSION EXPIRY ===');\nconsole.log('Session Valid:', isSessionValid);\nconsole.log('Phone:', currentMessageData.phone);\nconsole.log('Message:', messageText);\nconsole.log('Language:', lang);\nconsole.log('Branch:', branch);\nconsole.log('Last Class:', lastMentionedClass);\nconsole.log('Last Time:', lastMentionedTime);\nconsole.log('Latest Question:', latestQuestion);\n\nreturn [{\n  json: {\n    text: messageText,\n    phone: currentMessageData.phone || '',\n    sessionId: currentMessageData.phone || '',\n    sessionKey: currentMessageData.sessionKey || currentMessageData.phone || '',\n    senderWhats: currentMessageData.senderWhats || '',\n    botWhats: currentMessageData.botWhats || '',\n    profileName: currentMessageData.profileName || '',\n    isVoice: currentMessageData.isVoice || false,\n    voice: currentMessageData.voice || null,\n    isNewSession: !isSessionValid,\n    branch: branch,\n    lastMentionedClass: lastMentionedClass,\n    lastMentionedTime: lastMentionedTime,\n    latestQuestion: latestQuestion,\n    pendingBookings: pendingBookings,\n    surveySubmitted: surveySubmitted,\n    lang: { code: lang }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "56d298f4-fa8f-471d-b614-d73309dedd12",
      "name": "Format Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3744,
        1152
      ],
      "parameters": {
        "jsCode": "const agentOutput = $json.output || $json.text || '';\n\n// Get Normalize Message data properly\nconst normalizeNode = $node[\"Normalize Message\"];\nconst normalizeData = normalizeNode?.first?.()?.json || normalizeNode?.json || {};\n\nconsole.log('=== FORMAT RESPONSE DEBUG ===');\nconsole.log('Agent output:', agentOutput.substring(0, 200));\nconsole.log('Normalize data keys:', Object.keys(normalizeData));\nconsole.log('sessionKey:', normalizeData.sessionKey);\nconsole.log('phone:', normalizeData.phone);\nconsole.log('senderWhats:', normalizeData.senderWhats);\n\nreturn [{\n  json: {\n    replyText: agentOutput,\n    phone: normalizeData.sessionKey || normalizeData.phone || '',\n    senderWhats: normalizeData.senderWhats || '',\n    timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7323df07-189f-433a-8387-3aa4298b0e62",
      "name": "Send WhatsApp",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4096,
        1152
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "={{ $('Read Config').first().json.ultramsg_token }}"
            },
            {
              "name": "to",
              "value": "={{ $json.senderWhats }}"
            },
            {
              "name": "body",
              "value": "={{ $json.replyText }}"
            }
          ]
        }
      },
      "typeVersion": 4
    },
    {
      "id": "d756d11c-cb07-43a4-b217-dcda6130503f",
      "name": "Read Active Members",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1040,
        128
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1839231317,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CWcULthIhOq8kcaCVoVWJTkbKBSJ6aMum-jSt2_fQUc/edit#gid=1839231317",
          "cachedResultName": "Members"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "0cdd5103-38bd-42f4-9644-74414daccc82",
      "name": "Filter Active Members",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        128
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconsole.log('=== FILTER ACTIVE MEMBERS ===');\nconsole.log('Total members:', items.length);\n\nconst activeMembers = items.filter(item => {\n  const json = item.json;\n  \n  // Must have phone\n  if (!json.Phone && !json.phone) return false;\n  \n  // Must be active\n  const status = (json.status || json.Status || '').toLowerCase();\n  if (status && status !== 'active') return false;\n  \n  return true;\n});\n\nconsole.log('Active members to survey:', activeMembers.length);\n\nif (activeMembers.length === 0) {\n  return [];\n}\n\nreturn activeMembers.map(item => ({\n  json: {\n    phone: String(item.json.Phone || item.json.phone || '').replace(/\\D/g, '') + '@c.us',\n    name: item.json.name || item.json.Name || 'Member',\n    branch: item.json.branch || item.json.Branch || 'rabieh'\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "e8c3d4bf-436b-4ac2-8f67-f1fbe0a7dd12",
      "name": "Send Survey",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1408,
        128
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "={{ $('Read Config').first().json.ultramsg_token }}"
            },
            {
              "name": "to",
              "value": "={{ $json.phone }}"
            },
            {
              "name": "body",
              "value": "\"Hi! \ud83d\udc4b\n\nQuick survey from Stamina Gym \ud83d\udcaa\n\nPlease rate each from 1 to 4:\n1\ufe0f\u20e3 Very Satisfied  2\ufe0f\u20e3 Satisfied  3\ufe0f\u20e3 Neutral  4\ufe0f\u20e3 Unsatisfied\n\n1. Overall satisfaction with the gym\n2. Class variety\n3. Instructors quality\n4. Facilities & equipment\n5. Likelihood to recommend us\n\nReply with 5 numbers separated by commas, and optionally add a suggestion.\nExample: 1,2,1,3,2 or 1,2,1,3,2, great gym!\n\nThank you! \ud83d\ude4f\""
            }
          ]
        }
      },
      "typeVersion": 4
    },
    {
      "id": "f14613fa-574c-4309-a639-e595b4fb004b",
      "name": "Mark Survey Sent",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1600,
        128
      ],
      "parameters": {
        "columns": {
          "value": {
            "phone": "={{ $('Filter Active Members').item.json.phone.replace('@c.us', '') }}",
            "row_number": 0,
            "last_survey_date": "={{ new Date().toISOString() }}"
          },
          "schema": [
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "branch",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "branch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "membership_type",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "membership_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "join_date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "join_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_survey_date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "last_survey_date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "phone"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1839231317,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CWcULthIhOq8kcaCVoVWJTkbKBSJ6aMum-jSt2_fQUc/edit#gid=1839231317",
          "cachedResultName": "Members"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": false
    },
    {
      "id": "94f6d388-b9a7-4051-b74c-a7a0f4b2e43b",
      "name": "Parse Survey Response",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        416
      ],
      "parameters": {
        "jsCode": "// Check if already submitted\nconst surveySubmitted = ($json.SurveySubmitted || $json.surveySubmitted || '').toLowerCase();\nif (surveySubmitted === 'yes') {\n  return [{\n    json: {\n      ...$json,\n      valid: false,\n      alreadySubmitted: true,\n      replyText: \"You have already submitted your feedback. Thank you! \ud83d\ude4f\"\n    }\n  }];\n}\n\n// Read message from original WhatsApp input, not session lookup\nconst originalMsg = $('Detect Staff Survey Command').item.json;\nconst text = (originalMsg.text || $json.text || '').trim();\nconst phone = originalMsg.phone || originalMsg.sessionKey || $json.Phone || '';\nconst lang = originalMsg.lang?.code || $json.Lang || 'en';\nconst name = originalMsg.userName || originalMsg.profileName || 'Member';\nconst branch = $json.Branch || originalMsg.branch || '';\n\nconsole.log('=== PARSE SURVEY RESPONSE ===');\nconsole.log('Text:', text, 'Phone:', phone);\n\n// Extract all digits 1-4 from the message and any text after last digit as suggestion\n// Handles: \"1,2,3,4,2\", \"1,2,3,4,2 great gym\", \"1,2,3,4\", \"1 2 3 4 2\"\nconst tokens = text.split(/[,\\s]+/);\nconst ratings = [];\nlet suggestion = '';\nlet suggestionStarted = false;\n\nfor (const token of tokens) {\n  if (!suggestionStarted) {\n    const n = parseInt(token);\n    if (!isNaN(n) && n >= 1 && n <= 4 && ratings.length < 5) {\n      ratings.push(n);\n    } else if (token.length > 0) {\n      // Non-number token \u2014 rest is suggestion\n      suggestionStarted = true;\n      suggestion = tokens.slice(tokens.indexOf(token)).join(' ').trim();\n      break;\n    }\n  }\n}\n\nconsole.log('Ratings extracted:', ratings);\nconsole.log('Suggestion:', suggestion);\n\nif (ratings.length === 0) {\n  return [{\n    json: {\n      ...$json,\n      valid: false,\n      replyText: \"Please reply with numbers 1-4 separated by commas.\\nExample: 1,2,1,3,2\\n\\n1=Very Satisfied  2=Satisfied  3=Neutral  4=Unsatisfied \ud83d\ude0a\"\n    }\n  }];\n}\n\n// Fill missing ratings with empty string\nwhile (ratings.length < 5) ratings.push('');\n\nconst satisfactionMap = { 1: 'Very Satisfied', 2: 'Satisfied', 3: 'Neutral', 4: 'Unsatisfied' };\n\nreturn [{\n  json: {\n    ...$json,\n    valid: true,\n    ResponseID: 'SURV_' + Date.now(),\n    Phone: phone,\n    Name: name,\n    Branch: branch,\n    Q1: ratings[0],\n    Q2: ratings[1],\n    Q3: ratings[2],\n    Q4: ratings[3],\n    Q5: ratings[4],\n    Suggestion: suggestion,\n    Rating: ratings[0],\n    Satisfaction: satisfactionMap[ratings[0]] || '',\n    Language: lang,\n    SubmittedAt: new Date().toLocaleString('en-US', { timeZone: 'Asia/Beirut' }),\n    Status: 'completed'\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ecca0cb5-0c84-435c-8ee1-1bce89a60c9e",
      "name": "Append Survey Response",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1632,
        320
      ],
      "parameters": {
        "columns": {
          "value": {
            "Q1": "={{ $json.Q1 }}",
            "Q2": "={{ $json.Q2 }}",
            "Q3": "={{ $json.Q3 }}",
            "Q4": "={{ $json.Q4 }}",
            "Q5": "={{ $json.Q5 }}",
            "Name": "={{ $json.Name }}",
            "Phone": "={{ $json.Phone }}",
            "Branch": "={{ $json.Branch }}",
            "Rating": "={{ $json.Rating }}",
            "Status": "={{ $json.Status }}",
            "ResponseID": "={{ $json.ResponseID }}",
            "Suggestion": "={{ $json.Suggestion }}",
            "SubmittedAt": "={{ $json.SubmittedAt }}",
            "Satisfaction": "={{ $json.Satisfaction }}"
          },
          "schema": [
            {
              "id": "ResponseID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ResponseID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Branch",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Branch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Rating",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Satisfaction",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Satisfaction",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SubmittedAt",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "SubmittedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q1",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Q1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q2",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Q2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q3",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Q3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q4",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Q4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Q5",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Q5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Suggestion",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Suggestion",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 462947745,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CWcULthIhOq8kcaCVoVWJTkbKBSJ6aMum-jSt2_fQUc/edit#gid=462947745",
          "cachedResultName": "Survey_Responses"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "25e43a43-bef8-47b4-a2e0-99271f7266c8",
      "name": "Format Thank You",
      "type": "n8n-nodes-base.code",
      "position": [
        1808,
        320
      ],
      "parameters": {
        "jsCode": "const lang = $json.Language || 'en';\nconst suggestion = $json.Suggestion || '';\n\nlet msg = '';\nif (lang === 'ar') {\n  msg = '\u0634\u0643\u0631\u0627\u064b \u0639\u0644\u0649 \u062a\u0642\u064a\u064a\u0645\u0643! \ud83d\ude4f \u0631\u0623\u064a\u0643 \u064a\u0647\u0645\u0646\u0627 \u0643\u062a\u064a\u0631. \ud83d\udcaa';\n} else if (lang === 'fr') {\n  msg = 'Merci pour votre \u00e9valuation! \ud83d\ude4f Votre avis compte beaucoup pour nous. \ud83d\udcaa';\n} else {\n  msg = 'Thank you for your feedback! \ud83d\ude4f Your opinion means a lot to us. \ud83d\udcaa';\n}\n\nif (suggestion) {\n  msg += `\\n\\n\ud83d\udcdd We noted your suggestion: \"${suggestion}\"`;\n}\n\nreturn [{ json: { ...$json, replyText: msg, surveySubmitted: 'yes' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "27e34247-65ba-481e-a1d9-00ab1fcec8d5",
      "name": "Send Thank You",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1968,
        320
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "={{ $('Read Config').first().json.ultramsg_token }}"
            },
            {
              "name": "to",
              "value": "={{ $('Detect Staff Survey Command').item.json.senderWhats }}"
            },
            {
              "name": "body",
              "value": "={{ $json.replyText }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "17da0a9b-c6c0-4c5a-aed2-a33cfdfe92a8",
      "name": "Format Staff Notification",
      "type": "n8n-nodes-base.code",
      "position": [
        2160,
        320
      ],
      "parameters": {
        "jsCode": "// FORMAT STAFF NOTIFICATION\nconst rating = $('Parse Survey Response').item.json.Rating || 0;\nconst satisfaction = $('Parse Survey Response').item.json.Satisfaction || '';\nconst phone = $('Parse Survey Response').item.json.Phone || '';\nconst responseId = $('Parse Survey Response').item.json.ResponseID || '';\nconst name = $('Parse Survey Response').item.json.Name || 'Unknown';\n\nconsole.log('=== FORMAT STAFF NOTIFICATION ===');\nconsole.log('Rating:', rating, 'Phone:', phone);\n\n// Rating 1 = Very Satisfied (best), 4 = Unsatisfied (worst)\nconst ratingStars = rating === 1 ? '\u2b50\u2b50\u2b50\u2b50' : rating === 2 ? '\u2b50\u2b50\u2b50' : rating === 3 ? '\u2b50\u2b50' : '\u2b50';\nconst needsAttention = rating >= 3;\n\nconst staffMessage = `\ud83d\udcca *New Survey Response*\\n\\n` +\n  `\ud83d\udc64 Name: ${name}\\n` +\n  `\ud83d\udcf1 Phone: ${phone}\\n` +\n  `\u2b50 Rating: ${rating}/4 ${ratingStars}\\n` +\n  `\ud83d\ude0a Satisfaction: ${satisfaction}\\n` +\n  `\ud83c\udd94 Response ID: ${responseId}\\n\\n` +\n  `${needsAttention ? '\u26a0\ufe0f Needs attention!' : '\u2705 Great feedback!'}\\n\\n` +\n  `\ud83d\udd50 Submitted: ${new Date().toLocaleString('en-US', { timeZone: 'Asia/Beirut' })}`;\n\nreturn [{\n  json: {\n    ...$json,\n    staffMessage: staffMessage,\n    staffPhones: (() => {\n      const cfg = $('Read Config').first().json;\n      const br = ($json.branch || $json.Branch || '').toLowerCase();\n      return [br === cfg.branch_2 ? cfg.staff_phone_branch_2 : cfg.staff_phone_branch_1];\n    })()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "28ce69e5-c186-42c7-9e4e-aa94bd3d8620",
      "name": "Notify Staff Survey",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2560,
        320
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "={{ $('Read Config').first().json.ultramsg_token }}"
            },
            {
              "name": "to",
              "value": "={{ $json.chatId }}"
            },
            {
              "name": "body",
              "value": "={{ $json.message }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "85a6c632-92e0-4c08-932f-852d615d3bb2",
      "name": "Prepare Staff Listt",
      "type": "n8n-nodes-base.code",
      "position": [
        2368,
        320
      ],
      "parameters": {
        "jsCode": "// PREPARE STAFF NOTIFICATIONS\nconst message = $json.staffMessage;\nconst staffPhones = $json.staffPhones || [];\n\nconsole.log('=== PREPARE STAFF NOTIFICATIONS ===');\nconsole.log('Staff members:', staffPhones.length);\n\n// Create one item per staff member\nreturn staffPhones.map(phone => {\n  const whatsappId = String(phone).includes('@c.us') ? String(phone) : String(phone) + '@c.us';\n  return {\n    json: {\n      chatId: whatsappId,\n      message: message,\n      staffPhone: phone\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "e7789075-2dc1-4bcf-be56-f9903e597407",
      "name": "Detect Staff Survey Command",
      "type": "n8n-nodes-base.code",
      "position": [
        560,
        416
      ],
      "parameters": {
        "jsCode": "// DETECT STAFF COMMANDS\nconst text = ($json.text || '').trim().toLowerCase();\nconst phone = String($json.phone || $json.sessionKey || '');\n\nconsole.log('=== DETECT STAFF COMMAND ===');\nconsole.log('Text:', text);\nconsole.log('Phone:', phone);\n\n// Define staff phone numbers (without @c.us, just digits)\n// Per-branch staff phones \u2014 all staff can trigger survey\nconst cfg = $('Read Config').first().json;\nconst STAFF_PHONES = [\n  String(cfg.staff_phone_branch_1 || ''),\n  String(cfg.staff_phone_branch_2 || '')\n];\n\nconst isStaff = STAFF_PHONES.includes(phone);\n\nconsole.log('Is staff:', isStaff);\n\n// Check for survey command\nconst surveyCommands = [\n  'survey',\n  'send survey',\n  'start survey',\n  'trigger survey',\n  'questionnaire',\n  '\u0627\u0633\u062a\u0628\u064a\u0627\u0646',           // Arabic\n  '\u0627\u0633\u062a\u0645\u0627\u0631\u0629',          // Arabic\n  'sondage'           // French\n];\n\nconst isSurveyCommand = surveyCommands.some(cmd => \n  text === cmd || text.includes(cmd)\n);\n\nconsole.log('Is survey command:', isSurveyCommand);\n\nif (isStaff && isSurveyCommand) {\n  console.log('\u2705 STAFF SURVEY TRIGGER DETECTED!');\n  \n  return [{\n    json: {\n      ...$json,\n      isStaffCommand: true,\n      commandType: 'trigger_survey',\n      route: 'staff_trigger_survey'\n    }\n  }];\n}\n\n// Check for survey response (existing logic)\n// Detect survey response: starts with a digit 1-4, may have commas and text\nconst isSurveyResponse = /^[1-4][\\s,]/.test(text.trim()) || /^[1-4]$/.test(text.trim());\nconst sessionState = $json.sessionState || {};\nconst awaitingSurvey = sessionState.surveyAwaiting === true;\n\nif (isSurveyResponse) {\n  console.log('\u2705 Survey response detected!');\n  \n  return [{\n    json: {\n      ...$json,\n      isSurveyResponse: true,\n      surveyRating: parseInt(text),\n      route: 'survey_response'\n    }\n  }];\n}\n\n// Not a staff command or survey response\nreturn [{\n  json: {\n    ...$json,\n    isStaffCommand: false,\n    isSurveyResponse: false\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1ab10ef2-fb0a-4527-b070-1dff75f61171",
      "name": "Format Staff Confirmation",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        128
      ],
      "parameters": {
        "jsCode": "// FORMAT STAFF CONFIRMATION\nconst items = $input.all();\nconst surveyCount = items.length;\n\nconsole.log('=== FORMAT STAFF CONFIRMATION ===');\nconsole.log('Surveys sent:', surveyCount);\n\nconst confirmMessage = `\u2705 *Survey Sent Successfully!*\\n\\n` +\n  `\ud83d\udcca Sent to: ${surveyCount} active member(s)\\n\\n` +\n  `Members will receive:\\n` +\n  `\"Hi! \ud83d\udc4b Quick survey from Stamina Gym...\"\\n\\n` +\n  `\u23f0 ${new Date().toLocaleString('en-US', { timeZone: 'Asia/Beirut' })}\\n\\n` +\n  `You'll be notified when members respond! \ud83d\udcec`;\n\n// Get staff phone from the original trigger\nconst staffPhone = $('Detect Staff Survey Command').first().json.phone;\n\nreturn [{\n  json: {\n    chatId: staffPhone + '@c.us',\n    message: confirmMessage,\n    surveyCount: surveyCount\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f8637fe3-c153-4d5e-8fd3-9337527b759b",
      "name": "Send Staff Confirmation",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2000,
        128
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "token",
              "value": "={{ $('Read Config').first().json.ultramsg_token }}"
            },
            {
              "name": "to",
              "value": "={{ $json.chatId }}"
            },
            {
              "name": "body",
              "value": "={{ $json.message }}"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "8b4cc23d-288f-4f88-a1d6-0e1ef89e72ea",
      "name": "Switch: Staff Command or Survey",
      "type": "n8n-nodes-base.switch",
      "position": [
        768,
        400
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "5846478c-4419-4e10-a06d-9568474a15d3",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.isStaffCommand }}",
                    "rightValue": ""
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "267978ec-dec4-4a45-b30c-9e2cbab4ee52",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.isSurveyResponse }}",
                    "rightValue": ""
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "98135687-dcbb-46b8-a8c5-0df83be219fb",
      "name": "Determine Latest Question",
      "type": "n8n-nodes-base.code",
      "position": [
        4800,
        1168
      ],
      "parameters": {
        "jsCode": "// ===================================================================\n// DETERMINE LATEST QUESTION - FIXED\n// ===================================================================\n\nconst normalizeData = $node[\"Normalize Message\"].json;\nconst formatResponseData = $node[\"Format Response\"].json;\nconst lookupData = $node[\"Lookup Session\"].json || {};\nconst checkSessionData = $node[\"Check Session Expiry\"].json || {};\n\n// Get current message\nconst currentMessage = (normalizeData.text || '').toLowerCase().trim();\n\n// Get previous Latest_Question from Google Sheets\n// \u2705 FIXED: Always cast to string to prevent .toLowerCase() errors\nconst previousQuestion = String(checkSessionData.latestQuestion || '');\n\n// Get AI's response\nconst aiOutput = formatResponseData.replyText || '';\n\n// Get branch from session\nlet detectedBranch = checkSessionData.branch || null;\n\n// ===================================================================\n// EXTRACT lastMentionedClass and lastMentionedTime from AI response\n// \u2705 FIX: Match time to the SAME LINE as the class\n// ===================================================================\n\nlet lastMentionedClass = checkSessionData.lastMentionedClass || null;\nlet lastMentionedTime = checkSessionData.lastMentionedTime || null;\n\nconst classPatterns = ['trx', 'yoga', 'pilates', 'boxing', 'zumba', 'spinning', 'hiit', 'spin', 'dance'];\n\nfor (const cls of classPatterns) {\n  if (aiOutput.toLowerCase().includes(cls)) {\n    lastMentionedClass = cls;\n\n    const lines = aiOutput.split('\\n');\n    for (const line of lines) {\n      if (line.toLowerCase().includes(cls)) {\n        const timeOnLine = line.match(/\\b(\\d{1,2}:\\d{2}(?:\\s?[AP]M)?)\\b/i);\n        if (timeOnLine) {\n          lastMentionedTime = timeOnLine[1];\n        }\n        break;\n      }\n    }\n    break;\n  }\n}\n\nconsole.log('=== DETERMINE LATEST QUESTION ===');\nconsole.log('Current message:', currentMessage);\nconsole.log('Previous question:', previousQuestion);\nconsole.log('AI output (first 300):', aiOutput.substring(0, 300));\nconsole.log('Branch:', detectedBranch);\nconsole.log('Last Class:', lastMentionedClass);\nconsole.log('Last Time:', lastMentionedTime);\n\n// ===================================================================\n// STEP 1: If AI asked \"Which branch?\" \u2014 keep previousQuestion (covers ALL cases)\n// This handles greetings, typos, emojis \u2014 anything that triggered a branch ask\n// ===================================================================\n\nconst aiAskedBranch = (\n  aiOutput.toLowerCase().includes('which branch') ||\n  aiOutput.toLowerCase().includes('rabieh or bikfaya')\n);\n\nif (aiAskedBranch) {\n  // If previousQuestion is empty, the CURRENT message is the question to preserve\n  const greetingWords = ['hi', 'hello', 'hey', 'hii', 'hiii', 'heyy', 'heyyy', 'helloz', 'salam', '\u0645\u0631\u062d\u0628\u0627', '\u0623\u0647\u0644\u0627', 'bonjour', 'salut'];\n  const prevIsGreeting = greetingWords.includes(previousQuestion.toLowerCase().trim());\n  let questionToSave = (previousQuestion && !prevIsGreeting) ? previousQuestion : normalizeData.text;\n  // Strip leading greeting words from questionToSave so AI receives clean intent\n  // e.g. \"Hello classes plz\" \u2192 \"classes plz\"\n  const greetingPrefixPattern = /^(hi+|hello|hey+|heyy|heyyy|helloz|salam|\u0645\u0631\u062d\u0628\u0627|\u0623\u0647\u0644\u0627|bonjour|salut|bonsoir)[,!.\\s]+/i;\n  questionToSave = questionToSave.replace(greetingPrefixPattern, '').trim();\n  console.log('AI asked for branch \u2014 saving:', questionToSave);\n  return [{\n    json: {\n      ...formatResponseData,\n      latestQuestion: questionToSave,\n      branch: detectedBranch,\n      lastMentionedClass: lastMentionedClass,\n      lastMentionedTime: lastMentionedTime\n    }\n  }];\n}\n\nconst isGreeting = false; // placeholder kept for Step 5 reference\n\n// ===================================================================\n// STEP 4: Exact branch detection\n// ===================================================================\n\nconst validBranches = ['rabieh', 'bikfaya'];\n\nconst mentionedBranch = validBranches.find(b =>\n  currentMessage === b ||\n  currentMessage === b + ' branch' ||\n  currentMessage === 'the ' + b ||\n  currentMessage === 'the ' + b + ' branch'\n);\n\nconst isBranchAnswer = !!mentionedBranch;\n\nif (isBranchAnswer) {\n  detectedBranch = mentionedBranch;\n  console.log('Branch answer detected:', detectedBranch);\n}\n\nif (isBranchAnswer && previousQuestion) {\n  console.log('Keeping original question:', previousQuestion);\n\n  return [{\n    json: {\n      ...formatResponseData,\n      latestQuestion: previousQuestion,\n      branch: detectedBranch,\n      lastMentionedClass: lastMentionedClass,\n      lastMentionedTime: lastMentionedTime\n    }\n  }];\n}\n\n// ===================================================================\n// STEP 4b: \u2705 FIXED \u2014 User replied with a day name to a booking question\n// Check previousQuestion (not aiOutput) for booking intent\n// aiOutput is the CURRENT turn response, not the previous one\n// ===================================================================\n\nconst dayNames = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];\nconst isDay = dayNames.includes(currentMessage.trim());\n\nconst previousHasBooking = (\n  previousQuestion.toLowerCase().includes('book') ||\n  previousQuestion.toLowerCase().includes('reserve') ||\n  previousQuestion.toLowerCase().includes('\u0627\u062d\u062c\u0632')\n);\n\nif (isDay && previousHasBooking) {\n  // User answered \"which day?\" with a day name\n  // Combine: \"Book me yoga plz\" + \"Monday\" \u2192 \"Book me yoga plz on Monday\"\n  const combinedQuestion = previousQuestion + ' on ' + normalizeData.text;\n  console.log('\u2705 Day answer for booking \u2014 combining into:', combinedQuestion);\n\n  return [{\n    json: {\n      ...formatResponseData,\n      latestQuestion: combinedQuestion,\n      branch: detectedBranch,\n      lastMentionedClass: lastMentionedClass,\n      lastMentionedTime: lastMentionedTime\n    }\n  }];\n}\n\n// ===================================================================\n// STEP 5: Determine if short answer or new question\n// ===================================================================\n\nconst botAskedBooking = (\n  aiOutput.toLowerCase().includes('would you like to book') ||\n  aiOutput.toLowerCase().includes('shall i book') ||\n  aiOutput.toLowerCase().includes('want me to book')\n);\n\nconst isConfirmation = (\n  currentMessage === 'yes' ||\n  currentMessage === 'no' ||\n  currentMessage === 'sure' ||\n  currentMessage === 'ok' ||\n  currentMessage === 'okay' ||\n  currentMessage === 'yeah' ||\n  currentMessage === 'yep' ||\n  currentMessage === 'yup' ||\n  currentMessage === 'nope' ||\n  currentMessage === 'please' ||\n  currentMessage === 'book it' ||\n  currentMessage === 'go ahead' ||\n  currentMessage === '\u0646\u0639\u0645' ||\n  currentMessage === '\u0627\u0648\u0643' ||\n  currentMessage === '\u062a\u0645\u0627\u0645' ||\n  currentMessage === 'oui' ||\n  currentMessage === 'non'\n);\n\nconst isShortAnswer = previousQuestion && (\n  isConfirmation ||\n  (botAskedBooking && isConfirmation)\n);\n\nlet finalLatestQuestion;\n\nif (isShortAnswer) {\n  finalLatestQuestion = previousQuestion;\n  console.log('Short answer, keeping:', previousQuestion);\n} else if (isGreeting) {\n  finalLatestQuestion = previousQuestion || '';\n  console.log('Greeting, keeping previous:', finalLatestQuestion);\n} else {\n  const greetingWords = ['hi', 'hello', 'hey', 'hii', 'hiii', 'heyy', 'heyyy', 'salam', '\u0645\u0631\u062d\u0628\u0627', '\u0623\u0647\u0644\u0627', 'bonjour', 'salut', 'bonsoir'];\n  const isGreetingMsg = greetingWords.some(g => currentMessage === g);\n  // If branch already set and user sent a direct request \u2014 clear latestQuestion\n// No need to preserve it since branch is already known\nif (detectedBranch && !isShortAnswer) {\n  finalLatestQuestion = '';\n} else {\n  finalLatestQuestion = isGreetingMsg ? (previousQuestion || '') : normalizeData.text;\n}\n  console.log('New question:', finalLatestQuestion);\n}\n\n// ===================================================================\n// STEP 6: Detect branch from current message if not already set\n// ===================================================================\n\nif (!detectedBranch && isBranchAnswer) {\n  detectedBranch = mentionedBranch;\n}\n\n// \u2705 NEW: Also detect branch when user mentions it anywhere in the message\n// e.g. \"what classes at Rabieh\" or \"book yoga at Bikfaya\"\nif (!detectedBranch) {\n  for (const b of validBranches) {\n    if (currentMessage.includes(b)) {\n      detectedBranch = b;\n      console.log('Branch detected from message text:', detectedBranch);\n      break;\n    }\n  }\n}\n\nconsole.log('========================================');\nconsole.log('FINAL:');\nconsole.log('latestQuestion:', finalLatestQuestion);\nconsole.log('branch:', detectedBranch);\nconsole.log('lastMentionedClass:', lastMentionedClass);\nconsole.log('lastMentionedTime:', lastMentionedTime);\nconsole.log('========================================');\n\nreturn [{\n  json: {\n    ...formatResponseData,\n    latestQuestion: finalLatestQuestion,\n    branch: detectedBranch,\n    lastMentionedClass: lastMentionedClass,\n    lastMentionedTime: lastMentionedTime\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "5a4ab336-d66a-4787-8902-3834ca93357d",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        4240,
        1152
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "97b70fc2-7319-4648-a9f6-4a10045156f9",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Split Response').item.json.hasImage }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "28f69592-e6f9-484a-89b5-c4f0b2bbc521",
      "name": "Send Price List",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4496,
        1024
      ],
      "parameters": {
        "url": "={{ 'https://api.ultramsg.com/' + $('Read Config').first().json.ultramsg_instance + '/messages/chat' }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"token\": \"{{ $('Read Config').first().json.ultramsg_token }}\",\n  \"to\": \"{{ $('Split Response').item.json.senderWhats }}\",\n  \"image\": \"{{ $('Split Response').item.json.imageUrl }}\",\n  \"caption\": \"\ud83d\udccb Price List\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.3
    },
    {
      "id": "578a845b-5b56-433f-8893-a8ed9729697a",
      "name": "Cancel Booking",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        2768,
        1424
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "47svkah7woHaC8Tf",
          "cachedResultUrl": "/workflow/47svkah7woHaC8Tf",
          "cachedResultName": "Cancel Booking"
        },
        "description": "Cancel a gym class booking.\n\nThis tool handles ALL cases and always returns a final response:\n- If booking found and cancelled: returns cancellation confirmation\n- If booking not found: returns \"No active booking found\"  \n- If already cancelled: returns \"already cancelled\" message\n\nWhen you receive the response from this tool:\n1. The task_status field will say \"COMPLETE\"\n2. Send the response field directly to the user\n3. Your job is done - do not call this tool again under any circumstances\n4. Do not verify, do not check again, do not retry",
        "workflowInputs": {
          "value": {
            "date": "={{ $fromAI('date', 'Optional: specific date in YYYY-MM-DD format. Pass this when user specifies a day e.g. cancel yoga on Tuesday. Use pre-calculated dates from session context.', 'string') }}",
            "phone": "={{ $('Check Session Expiry').item.json.phone || '' }}",
            "branch": "={{ $('Check Session Expiry').item.json.branch || '' }}",
            "class_name": "={{ $fromAI('class_name', 'Optional: class name to cancel e.g. yoga, trx. Leave empty to cancel most recent booking.', 'string') }}"
          },
          "schema": [
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "branch",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "branch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "class_name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "class_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "dc8c531c-3a40-444f-956d-366490d1c5b2",
      "name": "Get Bookings",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        2896,
        1424
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "8AuMVeJN7DrXOxJF",
          "cachedResultUrl": "/workflow/8AuMVeJN7DrXOxJF",
          "cachedResultName": "Get Bookings"
        },
        "description": "Retrieve a user's confirmed gym class bookings. Use when user asks to see/view/show/list/check their bookings or reservations. Call ONCE, then send the response directly to the user and stop.",
        "workflowInputs": {
          "value": {
            "phone": "={{ $('Check Session Expiry').item.json.phone || '' }}",
            "branch": "={{ $('Check Session Expiry').item.json.branch || '' }}"
          },
          "schema": [
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "branch",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "branch",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "ae6126ce-651e-4fbf-b69d-dab17271b44f",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2880,
        1152
      ],
      "parameters": {
        "text": "={{ \"today: \" + $now.toFormat('yyyy-MM-dd') + \" (\" + $now.toFormat('cccc') + \")\\nnext Monday: \" + $now.plus({days: (8 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Tuesday: \" + $now.plus({days: (9 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Wednesday: \" + $now.plus({days: (10 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Thursday: \" + $now.plus({days: (11 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Friday: \" + $now.plus({days: (12 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Saturday: \" + $now.plus({days: (13 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nnext Sunday: \" + $now.plus({days: (14 - $now.weekday) % 7 || 7}).toFormat('yyyy-MM-dd') + \"\\nbranch: \" + ($json.branch || \"null\") + \"\\nlatestQuestion: \" + ($json.latestQuestion || \"\") + \"\\nphone: \" + $json.phone + \"\\nuserName: \" + ($json.profileName || \"WhatsApp User\") + \"\\nlastMentionedClass: \" + ($json.lastMentionedClass || \"null\") + \"\\nlastMentionedTime: \" + ($json.lastMentionedTime || \"null\") + \"\\npendingBookings: \" + JSON.stringify($json.pendingBookings || []) + \"\\n\\nUser message: \" + $json.text }}",
        "options": {
          "maxIterations": 10,
          "systemMessage": "## \u26a0\ufe0f TOOL COMPLETION RULE\nWhen ANY tool returns a response containing `done: true` or `status: completed` or `_stop: true`, you MUST:\n1. Send the response field to the user\n2. STOP - do not call any more tools\n3. Your turn is complete\n\n## \u26a0\ufe0f MANDATORY FLOW FOR EVERY QUESTION\n1. NEVER answer any gym question from your own knowledge without calling a tool first\n2. NEVER say \"I'll forward your message\" or \"I'll check with the team\" without actually calling Log Question\n3. If you find yourself about to answer without calling a tool \u2192 STOP and call the right tool first\n4. When the user replies with ONLY a branch name and latestQuestion is not empty \u2192 call the appropriate tool immediately using latestQuestion as the query \u2014 never respond conversationally without calling a tool\n\nFor CLASSES, SCHEDULE requests \u2192 call Classes tool directly (do NOT call FAQ first)\nFor LOCATION, ADDRESS, MAP requests \u2192 call Location tool directly (do NOT call FAQ first)\nFor BOOKING, CANCEL, MY BOOKINGS requests \u2192 call booking tools directly (do NOT call FAQ first)\nFor PRICING, POLICIES, FACILITIES, STAFF, or anything GENERAL \u2192 call FAQ tool first\nIf FAQ returns no useful answer \u2192 ALWAYS call Log Question immediately\n\n## \u26a0\ufe0f MODIFY ORDER IS MANDATORY \u2014 NEVER VIOLATE THIS\nFor ANY modification request (\"modify\", \"change\", \"move\", \"switch\", \"reschedule\"):\n1. Call Cancel Booking FIRST \u2014 always, no exceptions. For class_name, use ONLY the class name the user explicitly mentioned as the one to cancel/modify (e.g. \"modify my tabata\" \u2192 class_name: tabata). If the user only mentions a day (e.g. \"modify my Thursday booking\"), leave class_name empty and pass only the date. NEVER guess or invent a class name.\n2. Wait for Cancel Booking to complete successfully\n3. ONLY THEN call Create Booking for the new class\n4. NEVER call Create Booking

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

A complete WhatsApp AI chatbot that handles class bookings, cancellations, FAQ responses, schedule lookups, location queries, waitlist management, booking reminders, and staff notifications — all through WhatsApp in English, Arabic, or French. Receives WhatsApp messages via…

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

This workflow turns your WhatsApp Business number into a 24/7 AI-powered customer assistant — without any third-party chatbot platform. It receives incoming WhatsApp messages via Evolution API, unders

OpenAI, Information Extractor, Anthropic Chat +7
AI & RAG

⏺ 🚀 How it works

Agent, Anthropic Chat, Output Parser Structured +6
AI & RAG

CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.

Postgres, Mcp Client Tool, Google Drive Tool +14
AI & RAG

Remi 1.1. Uses lmChatOpenAi, memoryPostgresChat, openAi, postgres. Webhook trigger; 89 nodes.

OpenAI Chat, Memory Postgres Chat, OpenAI +7
AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10