{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c6364fe3-9260-4735-9628-11e04e729bfb",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4272,
        -240
      ],
      "parameters": {
        "width": 480,
        "height": 688,
        "content": "## Climate-Driven Lead Generation Engine for HVAC Companies\n\n\n### How it works\n\n1. The workflow triggers on a schedule to fetch contacts from the CRM. 2. It processes geocoding to update contact city locations. 3. The system groups contacts to retrieve local weather data. 4. It performs analysis to detect heat or snow wave anomalies. 5. Finally, it creates sales opportunities and sends WhatsApp notifications for qualified leads.\n\n### Setup steps\n\n- Configure the Schedule Trigger with the desired frequency.\n- Connect your CRM service to the contact and opportunity nodes.\n- Set up the WeatherAPI credential in the HTTP Request node.\n- Configure the WhatsApp business API credentials for sending messages.\n\n### Customization\n\nUpdate API keys for WeatherAPI, WhatsApp, and your CRM platform. Configure the threshold logic in the Code nodes to match your specific business rules."
      },
      "typeVersion": 1
    },
    {
      "id": "0155b0d0-fb62-45ac-bc3d-e5ae320346fd",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4832,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 304,
        "content": "## Initialize and fetch contacts\n\nThe entry point of the workflow which initiates the periodic data retrieval."
      },
      "typeVersion": 1
    },
    {
      "id": "6f845850-aafc-453c-814e-53002816a896",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5280,
        -112
      ],
      "parameters": {
        "color": 7,
        "height": 352,
        "content": "## Route and filter contacts\n\nHandles routing and initial processing of contact data."
      },
      "typeVersion": 1
    },
    {
      "id": "40c77d26-e3f1-436f-b7b1-584e0a55dffb",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5552,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 288,
        "content": "## Geolocate and update contacts\n\nGeolocates contacts based on address and handles looping logic."
      },
      "typeVersion": 1
    },
    {
      "id": "632eeab7-cc81-431c-84f5-368c0b58ebfc",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5552,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 304,
        "content": "## Process and forecast weather\n\nOrganizes contacts and retrieves local weather forecasts."
      },
      "typeVersion": 1
    },
    {
      "id": "f8bb7217-dae3-46d2-81a6-379bac639527",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5984,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 304,
        "content": "## Detect environmental trends\n\nDetects environmental trends to trigger follow-up actions."
      },
      "typeVersion": 1
    },
    {
      "id": "f2538943-244d-4e0f-bd38-71173032baf2",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6416,
        -240
      ],
      "parameters": {
        "color": 7,
        "width": 832,
        "height": 272,
        "content": "## Manage heat wave opportunities\n\nManages sales opportunities triggered by weather anomalies."
      },
      "typeVersion": 1
    },
    {
      "id": "6f7e8d0d-0838-492a-9e10-1c5cebd9d9af",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6432,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 848,
        "height": 272,
        "content": "## Manage snow wave opportunities\n\nManages sales opportunities for weather-related triggers."
      },
      "typeVersion": 1
    },
    {
      "id": "b4040010-3f56-43d1-ae88-f95e64976e56",
      "name": "Fetch Contacts",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        5088,
        80
      ],
      "parameters": {
        "filters": {},
        "options": {},
        "operation": "getAll",
        "returnAll": true,
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a20ce337-9364-4bf0-a030-c631aa19b323",
      "name": "Loop Over Contacts",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        5600,
        224
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "2a6c8b72-2a9a-46aa-9888-532f275a6146",
      "name": "Update Contact City",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        6048,
        240
      ],
      "parameters": {
        "contactId": "={{ $('Fetch Contacts').item.json.id }}",
        "operation": "update",
        "updateFields": {
          "city": "={{ $json.address.town }}"
        },
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "3e037868-892a-4193-94b3-934b11defddb",
      "name": "If City Set",
      "type": "n8n-nodes-base.if",
      "position": [
        5328,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ac11828b-f274-493e-a9c6-6dfa43c4ad11",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.city }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "1b86ac54-b7dc-4783-ab09-a8b7305b74fd",
      "name": "Group Contacts by City",
      "type": "n8n-nodes-base.code",
      "position": [
        5600,
        -96
      ],
      "parameters": {
        "jsCode": "// n8n Code Node (First one - Grouping by City)\nconst itemsData = $input.all();\nconst groupedCities = {};\n\nfor (const item of itemsData) {\n  const data = item.json;\n  const city = (data.city || \"Unknown\").trim().toLowerCase();\n\n  if (!groupedCities[city]) {\n    groupedCities[city] = {\n      city: city,\n      contacts: []\n    };\n  }\n  groupedCities[city].contacts.push(data);\n}\n\n// Output the grouped data\nreturn Object.values(groupedCities).map(cityGroup => {\n  return { json: cityGroup };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "2bf67e62-65dd-4c3c-a35f-1612acbeb0d2",
      "name": "Fetch Weather Forecast",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5808,
        -96
      ],
      "parameters": {
        "url": "https://api.weatherapi.com/v1/forecast.json",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.city }}"
            }
          ]
        }
      },
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "7d164349-eb95-477b-9bfe-7b554e95ed20",
      "name": "Fetch City from Address",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5824,
        240
      ],
      "parameters": {
        "url": "https://nominatim.openstreetmap.org/search",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.customFields[0].value }}"
            },
            {
              "name": "format",
              "value": "jsonv2"
            },
            {
              "name": "addressdetails",
              "value": "1"
            },
            {
              "name": "limit",
              "value": "1"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "MyApp/1.0 (your@email.com)"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "d80d3eee-3a89-49de-9038-708010ab5674",
      "name": "Detect Weather Hazards",
      "type": "n8n-nodes-base.code",
      "position": [
        6032,
        -96
      ],
      "parameters": {
        "jsCode": "// Safely loop over all weather items\nconst weatherItems = $input.all();\nconst contactsData = $('Group Contacts by City').all();\nconst results = [];\n\nconst seasonalSettings = {\n  0:  { heat: 70, cold: 25 }, // Jan\n  1:  { heat: 75, cold: 28 }, // Feb\n  2:  { heat: 80, cold: 32 }, // Mar\n  3:  { heat: 82, cold: 34 }, // Apr\n  4:  { heat: 85, cold: 36 }, // May\n  5:  { heat: 90, cold: 40 }, // Jun\n  6:  { heat: 95, cold: 45 }, // Jul\n  7:  { heat: 95, cold: 45 }, // Aug\n  8:  { heat: 90, cold: 40 }, // Sep\n  9:  { heat: 85, cold: 34 }, // Oct\n  10: { heat: 80, cold: 30 }, // Nov\n  11: { heat: 75, cold: 25 }  // Dec\n};\n\nfor (const item of weatherItems) {\n  const weatherData = item.json;\n\n  // Safety Check: Verify weather object has the expected structure\n  if (!weatherData || !weatherData.location || !weatherData.forecast) {\n    continue;\n  }\n\n  const cityName = weatherData.location.name.toLowerCase();\n  let cityContacts = [];\n\n  // Find contacts for this city from the grouped data\n  for (const contactGroup of contactsData) {\n    if (contactGroup.json.city && contactGroup.json.city.toLowerCase() === cityName) {\n      cityContacts = contactGroup.json.contacts;\n      break;\n    }\n  }\n\n  // Skip if no matching contacts found\n  if (cityContacts.length === 0) continue;\n\n  // Extract current month for seasonal threshold\n  const forecastDate = new Date(weatherData.location.localtime);\n  const currentMonth = forecastDate.getMonth(); \n\n  const HEATWAVE_THRESHOLD = seasonalSettings[currentMonth].heat;\n  const FREEZE_THRESHOLD = seasonalSettings[currentMonth].cold;\n\n  let isExtremeWeatherComing = false;\n  let weatherEventDetails = { type: null, targetDate: \"\", expectedTemp: null };\n\n  // Check forecast for extreme weather\n  const forecastDays = weatherData.forecast.forecastday;\n  for (const day of forecastDays) {\n    // Check Heat\n    if (day.day.maxtemp_f >= HEATWAVE_THRESHOLD) {\n      isExtremeWeatherComing = true;\n      weatherEventDetails.type = 'HEATWAVE';\n      weatherEventDetails.targetDate = day.date;\n      weatherEventDetails.expectedTemp = day.day.maxtemp_f;\n      break; \n    }\n    // Check Cold/Snow\n    else if (day.day.mintemp_f <= FREEZE_THRESHOLD || day.day.daily_will_it_snow === 1) {\n      isExtremeWeatherComing = true;\n      weatherEventDetails.type = 'FREEZE';\n      weatherEventDetails.targetDate = day.date;\n      weatherEventDetails.expectedTemp = day.day.mintemp_f;\n      break; \n    }\n  }\n\n  // Add mapped data to results if an event is detected\n  if (isExtremeWeatherComing) {\n    for (const contact of cityContacts) {\n      results.push({\n        json: {\n          contactId: contact.id || null,\n          email: contact.email || null,\n          phone: contact.phone || null,\n          city: weatherData.location.name,\n          trigger_date: weatherEventDetails.targetDate,\n          expected_temp: weatherEventDetails.expectedTemp,\n          campaign_type: weatherEventDetails.type,\n          threshold_used: weatherEventDetails.type === 'HEATWAVE' ? HEATWAVE_THRESHOLD : FREEZE_THRESHOLD\n        }\n      });\n    }\n  }\n}\n\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "b181818f-44b0-4560-aeea-94158f22b599",
      "name": "Wait 1 Second",
      "type": "n8n-nodes-base.wait",
      "position": [
        6256,
        240
      ],
      "parameters": {
        "amount": 1
      },
      "typeVersion": 1.1
    },
    {
      "id": "3b90f056-abaa-4e12-b876-42d678199601",
      "name": "Check Campaign Type",
      "type": "n8n-nodes-base.if",
      "position": [
        6240,
        -96
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "364199dd-33c1-4cd3-a1d3-4c2500e3dd9f",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.campaign_type }}",
              "rightValue": "HEATWAVE"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4df56dfb-17a1-44b7-84c7-372ac029df17",
      "name": "Create Template Opportunity",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        6464,
        -128
      ],
      "parameters": {
        "name": "={{ $('Fetch Contacts').item.json.contactName }} | HEATWAVE",
        "resource": "opportunity",
        "contactId": "={{ $json.contactId }}",
        "requestOptions": {},
        "additionalFields": {
          "stageId": ""
        }
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "828f33a5-55cb-48ab-9adf-80fdd96422a4",
      "name": "Create Message Opportunity",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        6480,
        176
      ],
      "parameters": {
        "name": "={{ $('Fetch Contacts').item.json.contactName }} | SNOWWAVE",
        "resource": "opportunity",
        "contactId": "={{ $json.contactId }}",
        "requestOptions": {},
        "additionalFields": {
          "stageId": ""
        }
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1e62fe8c-0d23-4da0-a4e6-d55957cad066",
      "name": "Update Template Opportunity",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        6896,
        -128
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": ""
        },
        "opportunityId": "={{ $('Create Template Opportunity').item.json.opportunity.id }}",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "70064d84-e476-4f98-962f-a8a676d1db8e",
      "name": "Update Message Opportunity",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        6928,
        176
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": ""
        },
        "opportunityId": "={{ $('Create Message Opportunity').item.json.opportunity.id }}",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f3033359-eaf8-4cc1-b89c-d71d5fb0792c",
      "name": "Send WhatsApp Template",
      "type": "n8n-nodes-base.whatsApp",
      "onError": "continueRegularOutput",
      "position": [
        6688,
        -128
      ],
      "parameters": {
        "template": "hvac|en",
        "components": {
          "component": [
            {
              "bodyParameters": {
                "parameter": [
                  {
                    "text": "={{ $('Fetch Contacts').item.json.firstName }}"
                  }
                ]
              }
            }
          ]
        },
        "phoneNumberId": "1083481144853090",
        "recipientPhoneNumber": "={{ $('Fetch Contacts').item.json.phone }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1,
      "alwaysOutputData": false
    },
    {
      "id": "a4ef992f-fa34-4676-8080-8e3a052bcb6e",
      "name": "Upsert Template Contact",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        7104,
        -128
      ],
      "parameters": {
        "email": "={{ $('Check Campaign Type').item.json.email }}",
        "requestOptions": {},
        "additionalFields": {
          "customFields": {
            "values": [
              {
                "fieldId": {
                  "__rl": true,
                  "mode": "list",
                  "value": "Db89nxN3F38g07fBdRWt",
                  "cachedResultName": "opp_type"
                },
                "fieldValue": "={{ $('Check Campaign Type').item.json.campaign_type }}"
              }
            ]
          }
        }
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b5722c88-5fed-40d2-af4e-bfc47cd926b0",
      "name": "Upsert Message Contact",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        7136,
        176
      ],
      "parameters": {
        "email": "={{ $('Check Campaign Type').item.json.email }}",
        "requestOptions": {},
        "additionalFields": {
          "customFields": {
            "values": [
              {
                "fieldId": {
                  "__rl": true,
                  "mode": "list",
                  "value": "Db89nxN3F38g07fBdRWt",
                  "cachedResultName": "opp_type"
                },
                "fieldValue": "={{ $('Check Campaign Type').item.json.campaign_type }}"
              }
            ]
          }
        }
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "531b7a44-28ea-4eb9-8587-e1265ddf121e",
      "name": "Every Morning at 7am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        4880,
        80
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "d5be704f-c33e-4d98-9731-449c9d528615",
      "name": "When WhatsApp Message Received",
      "type": "n8n-nodes-base.whatsAppTrigger",
      "position": [
        4848,
        1056
      ],
      "parameters": {
        "options": {},
        "updates": [
          "messages"
        ]
      },
      "credentials": {
        "whatsAppTriggerApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "e1780448-3ed1-4947-af20-70ac50d683f3",
      "name": "Fetch GHL Contacts",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        5264,
        1040
      ],
      "parameters": {
        "filters": {
          "query": "=+{{ $json.messages[0].from }}"
        },
        "options": {},
        "operation": "getAll",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "7dda8fea-f7d8-41fd-b119-ddab844ec7bd",
      "name": "Customer Service AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        6176,
        1360
      ],
      "parameters": {
        "text": "=First Name: {{ $json.firstName }}\nLast Name: {{ $json.lastName }}\nEmail: {{ $json.email }}\nPipeline Type: {{ $json.customFields[2].value }}\nContact ID: {{ $json.id }}\n\n<prompt>\n    <persona>\n        <name>Alex</name>\n        <role>Professional and friendly HVAC service coordinator</role>\n        <company>Blankarray HVAC Solutions</company>\n    </persona>\n\n    <context>\n        <timestamp>\n            The current date and time is {{ $now.toFormat(\"DDDD, HH:mm:ss ZZZZ\") }}. You must use this information to correctly interpret relative time requests from the user (like \"tomorrow morning,\" \"this Friday,\" or \"in two hours\").\n        </timestamp>\n        <user_details>\n            First Name: {{ $json.firstName }}\n            Last Name: {{ $json.lastName }}\n            Email: {{ $json.email }}\n            Pipeline Type: {{ $json.customFields[2].value }}\n            Contact ID: {{ $json.id }}\n        </user_details>\n    </context>\n\n    <instructions>\n        <task>\n            When a user interacts with you, determine their intent (scheduling service vs. rejecting service), fetch calendar availability, book appointments, and update the correct HighLevel pipeline stages based on the outcome and the user's specific Pipeline Type.\n        </task>\n        \n        <procedure>\n            <step n=\"1\">\n                <description>Determine Intent & Find Availability</description>\n                <condition case=\"user wants HVAC service\">\n                    <action>\n                        First, acknowledge their request warmly. Then use the 'Fetch Available Calendar Slots' tool to find available appointment times. \n                        \n                        CRITICAL - You must ALWAYS check availability first, even if the user suggests a specific time. Never book without first fetching available slots.\n                        \n                        To fetch slots:\n                        1. Convert the user's requested date to Unix timestamps in milliseconds\n                        2. Set Start_Date to the beginning of the day (00:00:00) in milliseconds\n                        3. Set End_Date to the end of the day (23:59:59) in milliseconds\n                        4. Example: For January 15, 2025:\n                           - Start_Date: 1736899200000 (Jan 15, 2025 00:00:00)\n                           - End_Date: 1736985599000 (Jan 15, 2025 23:59:59)\n                        \n                        After receiving available slots, present them to the user in a friendly, easy-to-read format with specific times.\n                    </action>\n                    <tool>Fetch Available Calendar Slots</tool>\n                </condition>\n                <condition case=\"user is not interested or hostile\">\n                    <action>\n                        If the user explicitly states they are not interested (e.g., \"not interested,\" \"stop messaging,\" \"leave me alone\"), politely acknowledge and apologize for the inconvenience.\n                        \n                        Then check the Pipeline Type and update accordingly:\n                        - If Pipeline Type = \"heatwave\" \u2192 use 'Close Heatwave Deal' tool\n                        - If Pipeline Type = \"snowwave\" \u2192 use 'Close Snowwave Deal' tool\n                        \n                        After updating, thank them and end the conversation professionally.\n                    </action>\n                    <tool>Close Heatwave Deal</tool>\n                    <tool>Close Snowwave Deal</tool>\n                </condition>\n            </step>\n            \n            <step n=\"2\">\n                <description>Book the Appointment</description>\n                <condition case=\"user agrees to a suggested time slot\">\n                    <action>\n                        Use the 'Book Calendar Appointment' tool to secure the appointment.\n                        \n                        CRITICAL - Format the Start_Time parameter correctly:\n                        1. Use ISO 8601 format with timezone offset\n                        2. Format: \"YYYY-MM-DDTHH:mm:ss+05:30\" (for India timezone)\n                        3. Example: \"2025-01-15T10:00:00+05:30\" for 10:00 AM on Jan 15, 2025\n                        4. The system automatically adds 30 minutes for the end time\n                        \n                        After successful booking, confirm the details clearly to the user.\n                    </action>\n                    <tool>Book Calendar Appointment</tool>\n                </condition>\n            </step>\n            \n            <step n=\"3\">\n                <description>Update the Pipeline</description>\n                <condition case=\"appointment is successfully booked\">\n                    <action>\n                        After confirming the booking with the user, update the pipeline status based on Pipeline Type:\n                        - If Pipeline Type = \"heatwave\" \u2192 use 'Check Heatwave Schedule' tool\n                        - If Pipeline Type = \"snowwave\" \u2192 use 'Scheduled Snowwave1' tool\n                        \n                        Do this silently - do not mention the pipeline update to the user.\n                    </action>\n                    <tool>Check Heatwave Schedule</tool>\n                    <tool>Scheduled Snowwave1</tool>\n                </condition>\n            </step>\n        </procedure>\n        \n        <tool_usage_guidelines>\n            <calendar_slots_tool>\n                Tool Name: Fetch Available Calendar Slots\n                \n                When to use: Whenever a user expresses interest in scheduling or mentions any date/time preference.\n                \n                Parameters:\n                - Start_Date: Unix timestamp in milliseconds for start of day (00:00:00)\n                - End_Date: Unix timestamp in milliseconds for end of day (23:59:59)\n                \n                Date conversion examples:\n                - \"tomorrow\" \u2192 Calculate tomorrow's date, convert to Unix ms timestamps\n                - \"next Monday\" \u2192 Find next Monday, convert to Unix ms timestamps\n                - \"January 20\" \u2192 Use current year (or next year if past), convert to Unix ms timestamps\n                \n                After receiving results: Present available times in a friendly, conversational format using bullet points.\n            </calendar_slots_tool>\n            \n            <booking_tool>\n                Tool Name: Book Calendar Appointment\n                \n                When to use: Only after user confirms a specific time slot from the available options.\n                \n                Parameters:\n                - Start_Time: ISO 8601 datetime string with timezone\n                  Format: \"YYYY-MM-DDTHH:mm:ss+05:30\"\n                  Example: \"2025-01-15T14:30:00+05:30\"\n                \n                The contactId is automatically populated from context, do not worry about it.\n                \n                After booking: Confirm the appointment date, time, and any relevant details warmly.\n            </booking_tool>\n            \n            <pipeline_tools>\n                These tools update the CRM pipeline stages. Use them silently based on Pipeline Type:\n                \n                For rejections (user not interested):\n                - \"heatwave\" \u2192 Close Heatwave Deal\n                - \"snowwave\" \u2192 Close Snowwave Deal\n                \n                For successful bookings:\n                - \"heatwave\" \u2192 Check Heatwave Schedule\n                - \"snowwave\" \u2192 Scheduled Snowwave1\n                \n                Never mention these internal updates to the user.\n            </pipeline_tools>\n        </tool_usage_guidelines>\n        \n        <general_instruction>\n            - Handle greetings naturally and warmly\n            - Be empathetic toward users dealing with temperature or comfort issues in their homes\n            - Always remain professional, even if the user becomes hostile\n            - NEVER book an appointment without first fetching available slots\n            - Keep responses conversational and friendly, avoiding technical jargon\n            - Use WhatsApp formatting (bold, italics, bullets) to make messages easy to read\n        </general_instruction>\n    </instructions>\n\n    <user_input>\n        <message>{{ $('When WhatsApp Message Received').item.json.messages[0].text.body }}</message>\n    </user_input>\n\n    <output_format>\n        <style>WhatsApp</style>\n        <rules>\n            <rule>Use *bold* for key information like dates and times</rule>\n            <rule>Use _italics_ for emphasis on important points</rule>\n            <rule>Use bullet lists (\u2022) for presenting multiple options</rule>\n            <rule>Keep responses short, clear, and conversational</rule>\n            <rule>Avoid markdown headers or code blocks</rule>\n            <rule>Use line breaks for readability</rule>\n            <rule>Use emojis sparingly and only when appropriate (\u2705 for confirmations, \ud83d\udcc5 for dates)</rule>\n        </rules>\n    </output_format>\n\n    <constraints>\n        <fallback>\n            If the answer cannot be found in the context, reply: \"I'm sorry, my primary role is to schedule HVAC service appointments. I don't have the information to answer that specific question. Is there anything related to scheduling I can help you with?\"\n        </fallback>\n    </constraints>\n</prompt>",
        "options": {},
        "promptType": "define"
      },
      "retryOnFail": true,
      "typeVersion": 3.1
    },
    {
      "id": "d4b6b223-d395-4b0e-97a7-05f1230a8838",
      "name": "Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        5744,
        1600
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "28957812-a52e-4667-b6cd-d5fb0b816cb1",
      "name": "Redis Chat History Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryRedisChat",
      "position": [
        5920,
        1600
      ],
      "parameters": {
        "sessionKey": "={{ $('When WhatsApp Message Received').first().json.messages[0].from }}",
        "sessionTTL": 500,
        "sessionIdType": "customKey",
        "contextWindowLength": 15
      },
      "credentials": {
        "redis": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.5
    },
    {
      "id": "f0947d2e-0211-473f-9a86-097881d1f400",
      "name": "If Not Stop Message",
      "type": "n8n-nodes-base.if",
      "position": [
        5952,
        1056
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6e0df4f7-4c94-4c39-a6b9-8b621d8cf18b",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('Fetch GHL Contacts').item.json.customFields[1].value }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "51089ee2-d2b3-4139-8063-edaf9b5e8952",
      "name": "Check GHL Contact Existence",
      "type": "n8n-nodes-base.if",
      "position": [
        5472,
        1056
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "27606252-d1dc-438d-b5e6-9d61a6bf061d",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "b16914e1-c6dc-43cd-9cde-f0d098da55b6",
      "name": "If Stop Command Received",
      "type": "n8n-nodes-base.if",
      "position": [
        5712,
        1040
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "9f926e21-1d24-4945-b667-6c920646f478",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('When WhatsApp Message Received').item.json.messages[0].text.body }}",
              "rightValue": "STOP"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "362f12d5-e7da-45e6-ab66-1790ea10bb95",
      "name": "Upsert GHL Contact",
      "type": "n8n-nodes-base.highLevel",
      "position": [
        5920,
        880
      ],
      "parameters": {
        "email": "={{ $json.email }}",
        "requestOptions": {},
        "additionalFields": {
          "customFields": {
            "values": [
              {
                "fieldId": {
                  "__rl": true,
                  "mode": "list",
                  "value": "WcwnHOE6Cx6Y48sGcd1R",
                  "cachedResultName": "stop_whatsapp"
                },
                "fieldValue": "true"
              }
            ]
          }
        }
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "c9c1e6e3-b5f6-4404-abc2-e8447ffabd51",
      "name": "Fetch Available Calendar Slots",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6112,
        1600
      ],
      "parameters": {
        "endDate": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('End_Date', ``, 'number') }}",
        "resource": "calendar",
        "operation": "getFreeSlots",
        "startDate": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start_Date', ``, 'number') }}",
        "calendarId": "enter-your-calender-id-here",
        "requestOptions": {},
        "descriptionType": "manual",
        "toolDescription": "Name: Fetch Available Calendar Slots\nDescription: Retrieves available appointment slots for a specific date range. \n- Start_Date: Unix timestamp in milliseconds (e.g., 1735689600000 for Jan 1, 2025)\n- End_Date: Unix timestamp in milliseconds\nReturns: List of available 30-minute time slots",
        "additionalFields": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1af10b36-0e27-46b1-b191-dd47444674dc",
      "name": "Book Calendar Appointment",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6304,
        1600
      ],
      "parameters": {
        "resource": "calendar",
        "contactId": "={{ $json.id }}",
        "startTime": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Start_Time', ``, 'string') }}",
        "calendarId": "enter-your-ghl-calender-id-here",
        "locationId": "enter-your-ghl-location-id-here",
        "requestOptions": {},
        "descriptionType": "manual",
        "toolDescription": "Name: Book appointment in a calendar in HighLevel\nDescription: Books an appointment at a specific time.\n- Start_Time: ISO 8601 datetime string (e.g., \"2025-01-15T10:00:00-05:00\")\n- contactId will be automatically populated from context",
        "additionalFields": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "7b7b9a80-6403-4d81-82ef-1ee4210b8305",
      "name": "Close Heatwave Deal",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6496,
        1600
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": ""
        },
        "opportunityId": "enter-your-opportunity-id-here",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "ab3e47a8-bdf9-4e91-9718-f7b05f42d3c2",
      "name": "Close Snowwave Deal",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6656,
        1600
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": ""
        },
        "opportunityId": "enter-your-opportunity-id-here",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9deadbde-c00f-42b2-b923-bc9167589bdd",
      "name": "Send WhatsApp Response",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        6528,
        1360
      ],
      "parameters": {
        "textBody": "={{ $json.output }}",
        "operation": "send",
        "phoneNumberId": "1083481144853090",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('When WhatsApp Message Received').first().json.messages[0].from }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "d95a81f7-8506-4bec-832f-c5c782d94ee0",
      "name": "If Valid Sender Exists",
      "type": "n8n-nodes-base.if",
      "position": [
        5056,
        1056
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "11bbaeec-d3f0-4e49-a323-10fb6e13a513",
              "operator": {
                "type": "number",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.messages[0].from }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "e826a77f-2d93-411e-b00d-577628ae5a20",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4256,
        992
      ],
      "parameters": {
        "width": 480,
        "height": 560,
        "content": "## AI-Powered Service Appointment Concierge\n\n\n### How it works\n\n1. Workflow triggers on incoming WhatsApp messages. 2. Contact details are verified via GoHighLevel CRM. 3. Input is checked for 'STOP' keywords to route accordingly. 4. An AI agent processes messages using conversation history. 5. CRM tools are executed and the final response is sent to the user.\n\n### Setup steps\n\n- - [ ] Configure WhatsApp credentials for the trigger and responder nodes.\n- - [ ] Connect your GoHighLevel account to the HighLevel nodes.\n- - [ ] Set up the Google Gemini API key for the AI agent.\n- - [ ] Configure a Redis instance for chat memory.\n\n### Customization\n\nYou can update the AI model settings, modify the HighLevel tool logic within the AI agent, or adjust the 'STOP' keyword filter in the IF node."
      },
      "typeVersion": 1
    },
    {
      "id": "4a90bcf6-fe98-4604-b4c2-f598440c2959",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4800,
        912
      ],
      "parameters": {
        "color": 7,
        "width": 816,
        "height": 320,
        "content": "## Trigger and validate contact information\n\nStarts the workflow upon receiving a WhatsApp message and verifies the contact against the GoHighLevel CRM."
      },
      "typeVersion": 1
    },
    {
      "id": "1588ccb0-6a38-4d22-8873-9673bdd285fe",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5664,
        720
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 496,
        "content": "## Filter and route incoming messages\n\nEvaluates incoming messages for 'STOP' keywords and handles contact updates or routes to the AI processing layer."
      },
      "typeVersion": 1
    },
    {
      "id": "44331225-f3b9-4457-8a5c-41accc844c59",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5696,
        1248
      ],
      "parameters": {
        "color": 7,
        "width": 1424,
        "height": 528,
        "content": "## AI processing and final response\n\nProcesses user queries via an AI agent equipped with HighLevel CRM tools and sends back the final response."
      },
      "typeVersion": 1
    },
    {
      "id": "2e69ded5-5f6b-4084-8369-137a5b1e85ec",
      "name": "Send template",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        6704,
        176
      ],
      "parameters": {
        "template": "hvacw|en",
        "components": {
          "component": [
            {
              "bodyParameters": {
                "parameter": [
                  {
                    "text": "={{ $('Fetch Contacts').item.json.firstName }}"
                  }
                ]
              }
            }
          ]
        },
        "phoneNumberId": "1083481144853090",
        "recipientPhoneNumber": "={{ $('Fetch Contacts').item.json.phone }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "395c6bac-bfd5-4a23-84d7-5802eebc6243",
      "name": "Schedule Heatwave",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6832,
        1600
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": "="
        },
        "opportunityId": "enter-your-opportunity-id-here",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "da69576e-66d8-4f0a-ad80-8f2184fc6454",
      "name": "Scheduled Snowwave",
      "type": "n8n-nodes-base.highLevelTool",
      "position": [
        6992,
        1600
      ],
      "parameters": {
        "resource": "opportunity",
        "operation": "update",
        "updateFields": {
          "stageId": "",
          "pipelineId": ""
        },
        "opportunityId": "enter-your-opportunity-id-here",
        "requestOptions": {}
      },
      "credentials": {
        "highLevelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "If City Set": {
      "main": [
        [
          {
            "node": "Group Contacts by City",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send template": {
      "main": [
        [
          {
            "node": "Update Message Opportunity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1 Second": {
      "main": [
        [
          {
            "node": "Loop Over Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Contacts": {
      "main": [
        [
          {
            "node": "If City Set",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Heatwave": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GHL Contacts": {
      "main": [
        [
          {
            "node": "Check GHL Contact Existence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Contacts": {
      "main": [
        [],
        [
          {
            "node": "Fetch City from Address",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scheduled Snowwave": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Check Campaign Type": {
      "main": [
        [
          {
            "node": "Create Template Opportunity",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create Message Opportunity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Close Heatwave Deal": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Close Snowwave Deal": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "If Not Stop Message": {
      "main": [
        [],
        [
          {
            "node": "Customer Service AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Contact City": {
      "main": [
        [
          {
            "node": "Wait 1 Second",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Morning at 7am": {
      "main": [
        [
          {
            "node": "Fetch Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Weather Hazards": {
      "main": [
        [
          {
            "node": "Check Campaign Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Weather Forecast": {
      "main": [
        [
          {
            "node": "Detect Weather Hazards",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group Contacts by City": {
      "main": [
        [
          {
            "node": "Fetch Weather Forecast",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Valid Sender Exists": {
      "main": [
        [
          {
            "node": "Fetch GHL Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send WhatsApp Template": {
      "main": [
        [
          {
            "node": "Update Template Opportunity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch City from Address": {
      "main": [
        [
          {
            "node": "Update Contact City",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Stop Command Received": {
      "main": [
        [
          {
            "node": "Upsert GHL Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "If Not Stop Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Book Calendar Appointment": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Customer Service AI Agent": {
      "main": [
        [
          {
            "node": "Send WhatsApp Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Redis Chat History Memory": {
      "ai_memory": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Create Message Opportunity": {
      "main": [
        [
          {
            "node": "Send template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Message Opportunity": {
      "main": [
        [
          {
            "node": "Upsert Message Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check GHL Contact Existence": {
      "main": [
        [
          {
            "node": "If Stop Command Received",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Template Opportunity": {
      "main": [
        [
          {
            "node": "Send WhatsApp Template",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Template Opportunity": {
      "main": [
        [
          {
            "node": "Upsert Template Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Available Calendar Slots": {
      "ai_tool": [
        [
          {
            "node": "Customer Service AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "When WhatsApp Message Received": {
      "main": [
        [
          {
            "node": "If Valid Sender Exists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}