{
  "id": "lVVJ4qFcAEMZ65qs",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "US AQI \u2014 Daily 6:30AM Aggregate, Score, Alert, Forecast (blankarray)",
  "tags": [],
  "nodes": [
    {
      "id": "b0f1a339-fa14-4a9f-a6fc-4b24c8bed18c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1776,
        -224
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Untitled workflow\n\n### How it works\n\nThe workflow triggers daily to fetch air quality data from Open-Meteo and AirNow.\nIt reconciles the data from both sources into a weighted average AQI.\nAn AI agent assesses the risk based on the AQI and the user's specific health profile.\nThe workflow deduplicates identical alerts to avoid spamming the user.\nIt routes the final alert based on severity (moderate, severe, or hazardous).\nAlerts are sent across multiple channels including WhatsApp, SMS, and Slack depending on urgency.\n\n### Setup steps\n\n- [ ] Configure Open-Meteo and AirNow API credentials if required by the HTTP Request nodes.\n- [ ] Set up Google Gemini API credentials for the AI Agent risk scoring.\n- [ ] Authenticate WhatsApp, Twilio, and Slack nodes for sending notifications.\n- [ ] Update the user health profile and city parameters in the Set nodes with your personal details.\n\n### Customization\n\nYou can adjust the daily schedule in the Cron trigger or modify the severity thresholds in the Router node."
      },
      "typeVersion": 1
    },
    {
      "id": "72488bcc-c49e-414a-b9c4-59681e0590c2",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2336,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Trigger and Configuration\n\nStarts the workflow daily and configures location parameters."
      },
      "typeVersion": 1
    },
    {
      "id": "0faca880-1ac4-4870-816e-5d19313c8717",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2784,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Fetch Open-Meteo Data\n\nRetrieves and normalizes current AQI data from Open-Meteo."
      },
      "typeVersion": 1
    },
    {
      "id": "67b3e1b8-aad0-406f-a3e1-58338d525ecd",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3232,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Fetch AirNow Data\n\nRetrieves and normalizes air quality data from AirNow."
      },
      "typeVersion": 1
    },
    {
      "id": "0bbc925a-d078-4d9a-ab68-5ed0db905b77",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3680,
        -112
      ],
      "parameters": {
        "color": 7,
        "height": 320,
        "content": "## Reconcile AQI Data\n\nCalculates a weighted average AQI from multiple sources."
      },
      "typeVersion": 1
    },
    {
      "id": "0a39479e-876a-440c-ac83-e89a5598c119",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3952,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 304,
        "content": "## Prepare Health Profile\n\nSets the user's health profile and formats the risk scoring input."
      },
      "typeVersion": 1
    },
    {
      "id": "73bc99bd-dda3-4049-b36a-dc2b2d5fa977",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4400,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 592,
        "height": 480,
        "content": "## AI Risk Scoring\n\nAnalyzes AQI and health profile using AI to output a risk tier."
      },
      "typeVersion": 1
    },
    {
      "id": "70833e76-ecdb-43d8-a3d6-e6950c738414",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5024,
        -224
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 544,
        "content": "## Deduplication and Routing\n\nChecks for duplicate alerts and routes notifications based on severity."
      },
      "typeVersion": 1
    },
    {
      "id": "0834cd2f-28a7-4ac6-b6be-2176b41e7fff",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5696,
        -224
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## Moderate Alert Notifications\n\nSends a WhatsApp message for moderate AQI levels."
      },
      "typeVersion": 1
    },
    {
      "id": "1bdb7be9-883d-4dd8-afc8-9dd71ea6df62",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5696,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 272,
        "content": "## Severe Alert Notifications\n\nSends WhatsApp, SMS, and Slack alerts for severe AQI levels."
      },
      "typeVersion": 1
    },
    {
      "id": "9e060fe9-4b6c-41e1-8a93-8d7c88a2d476",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5696,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 272,
        "content": "## Hazardous Alert Notifications\n\nSends urgent WhatsApp, SMS, and Slack alerts for hazardous AQI levels."
      },
      "typeVersion": 1
    },
    {
      "id": "707f2980-d1ed-480a-a36d-47ccddcc8224",
      "name": "Set City Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        2608,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "city_name",
              "type": "string",
              "value": "Austin, TX"
            },
            {
              "id": "a2",
              "name": "latitude",
              "type": "number",
              "value": 30.2672
            },
            {
              "id": "a3",
              "name": "longitude",
              "type": "number",
              "value": -97.7431
            },
            {
              "id": "a4",
              "name": "airnow_rss_url",
              "type": "string",
              "value": "https://feeds.airnowapi.org/rss/realtime/7.xml"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e8f9cd6a-a882-4742-8040-951ab414aac1",
      "name": "Fetch Open-Meteo AQI",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "No official n8n node exists for Open-Meteo. Free, no API key required.",
      "position": [
        2832,
        48
      ],
      "parameters": {
        "url": "https://air-quality-api.open-meteo.com/v1/air-quality",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "latitude",
              "value": "={{ $json.latitude }}"
            },
            {
              "name": "longitude",
              "value": "={{ $json.longitude }}"
            },
            {
              "name": "current",
              "value": "us_aqi,pm2_5,pm10,ozone"
            },
            {
              "name": "timezone",
              "value": "auto"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e378e3cc-318e-4807-bd32-6551dadca8a1",
      "name": "Set User Health Profile",
      "type": "n8n-nodes-base.set",
      "position": [
        4000,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d1",
              "name": "age",
              "type": "number",
              "value": 34
            },
            {
              "id": "d2",
              "name": "has_asthma_copd",
              "type": "boolean",
              "value": true
            },
            {
              "id": "d3",
              "name": "is_pregnant",
              "type": "boolean",
              "value": false
            },
            {
              "id": "d4",
              "name": "is_outdoor_worker",
              "type": "boolean",
              "value": false
            },
            {
              "id": "d5",
              "name": "has_pets",
              "type": "boolean",
              "value": true
            },
            {
              "id": "d6",
              "name": "is_elderly_or_child",
              "type": "boolean",
              "value": false
            },
            {
              "id": "d7",
              "name": "whatsapp_number",
              "type": "string",
              "value": "+1234567890"
            },
            {
              "id": "d8",
              "name": "sms_number",
              "type": "string",
              "value": "+1234567890"
            },
            {
              "id": "d9",
              "name": "caregiver_slack_channel",
              "type": "string",
              "value": "aqi-caregiver-alerts"
            },
            {
              "id": "d10",
              "name": "city_slack_channel",
              "type": "string",
              "value": "aqi-city-hazardous"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "b7647689-7262-4800-9303-d586e15be119",
      "name": "Prepare Risk Scoring Data",
      "type": "n8n-nodes-base.set",
      "position": [
        4224,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "p1",
              "name": "city_name",
              "type": "string",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.city_name }}"
            },
            {
              "id": "p2",
              "name": "final_aqi",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.final_aqi }}"
            },
            {
              "id": "p3",
              "name": "aqi_bucket",
              "type": "string",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.aqi_bucket }}"
            },
            {
              "id": "p4",
              "name": "dominant_pollutant",
              "type": "string",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.dominant_pollutant }}"
            },
            {
              "id": "p5",
              "name": "pm2_5",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.pm2_5 }}"
            },
            {
              "id": "p6",
              "name": "pm10",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.pm10 }}"
            },
            {
              "id": "p7",
              "name": "ozone",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.ozone }}"
            },
            {
              "id": "p8",
              "name": "latitude",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.latitude }}"
            },
            {
              "id": "p9",
              "name": "longitude",
              "type": "number",
              "value": "={{ $('Calculate Weighted Avg AQI').item.json.longitude }}"
            },
            {
              "id": "p10",
              "name": "age",
              "type": "number",
              "value": "={{ $('Set User Health Profile').item.json.age }}"
            },
            {
              "id": "p11",
              "name": "has_asthma_copd",
              "type": "boolean",
              "value": "={{ $('Set User Health Profile').item.json.has_asthma_copd }}"
            },
            {
              "id": "p12",
              "name": "is_pregnant",
              "type": "boolean",
              "value": "={{ $('Set User Health Profile').item.json.is_pregnant }}"
            },
            {
              "id": "p13",
              "name": "is_outdoor_worker",
              "type": "boolean",
              "value": "={{ $('Set User Health Profile').item.json.is_outdoor_worker }}"
            },
            {
              "id": "p14",
              "name": "has_pets",
              "type": "boolean",
              "value": "={{ $('Set User Health Profile').item.json.has_pets }}"
            },
            {
              "id": "p15",
              "name": "whatsapp_number",
              "type": "string",
              "value": "={{ $('Set User Health Profile').item.json.whatsapp_number }}"
            },
            {
              "id": "p16",
              "name": "sms_number",
              "type": "string",
              "value": "={{ $('Set User Health Profile').item.json.sms_number }}"
            },
            {
              "id": "p17",
              "name": "caregiver_slack_channel",
              "type": "string",
              "value": "={{ $('Set User Health Profile').item.json.caregiver_slack_channel }}"
            },
            {
              "id": "p18",
              "name": "city_slack_channel",
              "type": "string",
              "value": "={{ $('Set User Health Profile').item.json.city_slack_channel }}"
            },
            {
              "id": "p19",
              "name": "is_elderly_or_child",
              "type": "boolean",
              "value": "={{ $('Set User Health Profile').item.json.is_elderly_or_child }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fa7d5c9e-4154-4b43-879c-36265469cb88",
      "name": "Risk Scoring Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "Official LangChain AI Agent node, backed by Gemini Chat Model + Structured Output Parser below.",
      "position": [
        4496,
        48
      ],
      "parameters": {
        "text": "=You are an air quality health advisor. Given this AQI reading and user health profile, produce a risk tier and a specific, actionable advisory (mention the dominant pollutant and correct protective gear, e.g. N95 vs surgical mask - not generic 'stay indoors').\n\nAQI DATA:\nCity: {{ $json.city_name }}\nFinal AQI: {{ $json.final_aqi }} ({{ $json.aqi_bucket }})\nDominant pollutant: {{ $json.dominant_pollutant }}\nPM2.5: {{ $json.pm2_5 }}, PM10: {{ $json.pm10 }}, Ozone: {{ $json.ozone }}\n\nUSER PROFILE:\nAge: {{ $json.age }}, Asthma/COPD: {{ $json.has_asthma_copd }}, Pregnant: {{ $json.is_pregnant }}, Outdoor worker: {{ $json.is_outdoor_worker }}, Has pets: {{ $json.has_pets }}",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "153e560d-7e4f-4176-9ea6-c1a0902f3d32",
      "name": "Gemini AI Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        4448,
        272
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-3.1-flash-lite"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "f06f1d4f-6239-4698-8087-e9a7359f4411",
      "name": "Parse Risk Output Structure",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        4640,
        272
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "={\n  \"type\": \"object\",\n  \"properties\": {\n    \"risk_tier\": { \"type\": \"string\", \"enum\": [\"low\", \"moderate\", \"severe\", \"hazardous\"] },\n    \"advisory_text\": { \"type\": \"string\" },\n    \"reasoning\": { \"type\": \"string\" }\n  },\n  \"required\": [\"risk_tier\", \"advisory_text\", \"reasoning\"]\n}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "bd25920f-e220-4b2f-b90c-cef4e9a2c23a",
      "name": "Map Risk Results",
      "type": "n8n-nodes-base.set",
      "position": [
        4848,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "q1",
              "name": "risk_tier",
              "type": "string",
              "value": "={{ $json.output.risk_tier }}"
            },
            {
              "id": "q2",
              "name": "advisory_text",
              "type": "string",
              "value": "={{ $json.output.advisory_text }}"
            },
            {
              "id": "q3",
              "name": "city_name",
              "type": "string",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.city_name }}"
            },
            {
              "id": "q4",
              "name": "final_aqi",
              "type": "number",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.final_aqi }}"
            },
            {
              "id": "q5",
              "name": "latitude",
              "type": "number",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.latitude }}"
            },
            {
              "id": "q6",
              "name": "longitude",
              "type": "number",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.longitude }}"
            },
            {
              "id": "q7",
              "name": "whatsapp_number",
              "type": "string",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.whatsapp_number }}"
            },
            {
              "id": "q8",
              "name": "sms_number",
              "type": "string",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.sms_number }}"
            },
            {
              "id": "q9",
              "name": "caregiver_slack_channel",
              "type": "string",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.caregiver_slack_channel }}"
            },
            {
              "id": "q10",
              "name": "city_slack_channel",
              "type": "string",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.city_slack_channel }}"
            },
            {
              "id": "q11",
              "name": "is_elderly_or_child",
              "type": "boolean",
              "value": "={{ $('Prepare Risk Scoring Data').item.json.is_elderly_or_child }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2ccdd6f3-6b88-4564-931c-e7199f14692f",
      "name": "Check Duplicate Alert Status",
      "type": "n8n-nodes-base.code",
      "position": [
        5072,
        48
      ],
      "parameters": {
        "jsCode": "const risk = $('Map Risk Results').item.json;\nconst staticData = $getWorkflowStaticData('global');\n\nconst today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD\nconst key = risk.city_name.replace(/\\s/g, '_');\n\nstaticData.lastAlerts = staticData.lastAlerts || {};\nconst last = staticData.lastAlerts[key];\n\n// Only skip if we already alerted on the SAME tier for the SAME calendar day\nconst isDuplicate = last && last.date === today && last.tier === risk.risk_tier;\n\nif (!isDuplicate) {\n  staticData.lastAlerts[key] = { date: today, tier: risk.risk_tier };\n}\n\nreturn [{\n  json: {\n    ...risk,\n    should_alert: !isDuplicate,\n    previous_tier: last ? last.tier : null,\n    previous_alert_date: last ? last.date : null\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1db0bbf5-7a49-45a4-9bcf-ecb22a7be361",
      "name": "If Alert Needed",
      "type": "n8n-nodes-base.if",
      "position": [
        5296,
        48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.should_alert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "bb1760f6-cbd2-4b5e-951b-0ff39f8d10d3",
      "name": "Do Nothing on Duplicate",
      "type": "n8n-nodes-base.noOp",
      "position": [
        5488,
        -112
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "efe83959-7cff-4df1-a52a-7c20cf33bbbc",
      "name": "Route by Severity",
      "type": "n8n-nodes-base.switch",
      "position": [
        5520,
        128
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "g1",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Map Risk Results').item.json.risk_tier }}",
                    "rightValue": "moderate"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "g2",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Map Risk Results').item.json.risk_tier }}",
                    "rightValue": "severe"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "g3",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $('Map Risk Results').item.json.risk_tier }}",
                    "rightValue": "hazardous"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "cb72522a-10b2-4dca-b428-562331fcadda",
      "name": "Send WhatsApp Moderate Alert",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        5744,
        -48
      ],
      "parameters": {
        "textBody": "=AQI Update for {{ $('Map Risk Results').item.json.city_name }}: {{ $('Map Risk Results').item.json.final_aqi }}. {{ $('Map Risk Results').item.json.advisory_text }}",
        "operation": "send",
        "phoneNumberId": "1083481144853090",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Map Risk Results').item.json.whatsapp_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7fe56693-0fdb-4403-9b83-bd45516a1f52",
      "name": "Send WhatsApp Severe Alert",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        5744,
        256
      ],
      "parameters": {
        "textBody": "=SEVERE AQI ALERT for {{ $('Map Risk Results').item.json.city_name }}: {{ $('Map Risk Results').item.json.final_aqi }}. {{ $('Map Risk Results').item.json.advisory_text }}",
        "operation": "send",
        "phoneNumberId": "1083481144853090",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Map Risk Results').item.json.whatsapp_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d21ff052-ff52-4581-a2b7-a95b3e840e2d",
      "name": "Send Twilio SMS Severe Alert",
      "type": "n8n-nodes-base.twilio",
      "position": [
        5968,
        256
      ],
      "parameters": {
        "to": "={{ $('Map Risk Results').item.json.sms_number }}",
        "from": "YOUR_TWILIO_NUMBER",
        "message": "=SEVERE AQI in {{ $('Map Risk Results').item.json.city_name }}: {{ $('Map Risk Results').item.json.final_aqi }}. {{ $('Map Risk Results').item.json.advisory_text }}",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "410eb60d-8b34-4fd8-86b6-8d94f5025432",
      "name": "Post Severe Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        6192,
        256
      ],
      "parameters": {
        "text": "=Severe AQI ({{ $('Map Risk Results').item.json.final_aqi }}) in {{ $('Map Risk Results').item.json.city_name }} - elderly/child profile: {{ $('Map Risk Results').item.json.is_elderly_or_child }}. {{ $('Map Risk Results').item.json.advisory_text }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Map Risk Results').item.json.caregiver_slack_channel }}"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "d6690ab0-96e6-4d93-bcc1-87bec650d1b7",
      "name": "Send WhatsApp Hazardous Alert",
      "type": "n8n-nodes-base.whatsApp",
      "position": [
        5744,
        560
      ],
      "parameters": {
        "textBody": "=\ud83d\udea8 HAZARDOUS AQI EMERGENCY in {{ $('Map Risk Results').item.json.city_name }}: {{ $('Map Risk Results').item.json.final_aqi }}. Please execute your emergency respiratory plan immediately. {{ $('Map Risk Results').item.json.advisory_text }}",
        "operation": "send",
        "phoneNumberId": "YOUR_PHONE_NUMBER_ID",
        "additionalFields": {},
        "recipientPhoneNumber": "={{ $('Map Risk Results').item.json.whatsapp_number }}"
      },
      "credentials": {
        "whatsAppApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "83264319-b802-4466-87c4-26a046303068",
      "name": "Send Twilio SMS Hazardous Alert",
      "type": "n8n-nodes-base.twilio",
      "position": [
        5968,
        560
      ],
      "parameters": {
        "to": "={{ $('Map Risk Results').item.json.sms_number }}",
        "from": "YOUR_TWILIO_NUMBER",
        "message": "=\ud83d\udea8 HAZARDOUS AQI ({{ $('Map Risk Results').item.json.final_aqi }}). Immediate action required. Your caregiver has been notified.",
        "options": {}
      },
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bd453416-7fdd-454a-8252-df1fa94c0bdf",
      "name": "Post Urgent Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        6192,
        560
      ],
      "parameters": {
        "text": "=<!channel> \ud83d\udea8 HAZARDOUS AQI DETECTED ({{ $('Map Risk Results').item.json.final_aqi }}) in {{ $('Map Risk Results').item.json.city_name }}. An automated emergency alert has been dispatched to the user. Please check in on them immediately. Asthma/COPD Profile: {{ $('Map Risk Results').item.json.has_asthma_copd }}.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Map Risk Results').item.json.caregiver_slack_channel }}"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "f3d47f11-81cb-418f-aeb0-283f81f3a89f",
      "name": "Calculate Weighted Avg AQI",
      "type": "n8n-nodes-base.code",
      "position": [
        3728,
        48
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set City Configuration').first()?.json || { city_name: 'Unknown', latitude: null, longitude: null };\nconst om = $('Normalize Open-Meteo Data').first()?.json || {};\nconst an = $('Normalize AirNow Data').first()?.json || {};\n\nconst a = om.source_a_aqi;\nconst b = an.source_b_aqi;\n\nlet finalAqi, sourcesUsed;\nif (a != null && b != null) {\n  finalAqi = Math.round((a * 0.4) + (b * 0.6));\n  sourcesUsed = 'open-meteo+airnow';\n} else if (b != null) {\n  finalAqi = Math.round(b);\n  sourcesUsed = 'airnow-only';\n} else if (a != null) {\n  finalAqi = Math.round(a);\n  sourcesUsed = 'open-meteo-only';\n} else {\n  finalAqi = null;\n  sourcesUsed = 'no-data';\n}\n\nfunction bucket(aqi) {\n  if (aqi == null) return 'unknown';\n  if (aqi <= 50) return 'good';\n  if (aqi <= 100) return 'moderate';\n  if (aqi <= 150) return 'unhealthy_sensitive';\n  if (aqi <= 200) return 'unhealthy';\n  if (aqi <= 300) return 'very_unhealthy';\n  return 'hazardous';\n}\n\nconst pollutants = { pm2_5: om.source_a_pm25, pm10: om.source_a_pm10, ozone: om.source_a_ozone };\nconst dominant = Object.entries(pollutants)\n  .filter(([,v]) => v != null)\n  .sort((x,y) => y[1]-x[1])[0]?.[0] || an.source_b_pollutant || 'unknown';\n\nreturn [{\n  json: {\n    city_name: cfg.city_name,\n    latitude: cfg.latitude,\n    longitude: cfg.longitude,\n    final_aqi: finalAqi,\n    aqi_bucket: bucket(finalAqi),\n    dominant_pollutant: dominant,\n    sources_used: sourcesUsed,\n    pm2_5: om.source_a_pm25,\n    pm10: om.source_a_pm10,\n    ozone: om.source_a_ozone,\n    reading_timestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3bd44faa-698c-469c-9b55-4a6eaa978114",
      "name": "Normalize AirNow Data",
      "type": "n8n-nodes-base.code",
      "position": [
        3504,
        48
      ],
      "parameters": {
        "jsCode": "const data = $input.first()?.json || {};\nconst text = typeof data.data === 'string' ? data.data : JSON.stringify(data);\n\nlet aqi = null;\nlet pollutant = 'unknown';\n\nconst aqiMatch = text.match(/(\\d+)\\s*AQI\\s*-\\s*([^<]+)/i);\n\nif (aqiMatch) {\n  aqi = parseInt(aqiMatch[1], 10);\n  \n  const pollStr = aqiMatch[2].toLowerCase();\n  if (pollStr.includes('2.5')) pollutant = 'PM2.5';\n  else if (pollStr.includes('10')) pollutant = 'PM10';\n  else if (pollStr.includes('ozone')) pollutant = 'Ozone';\n  else pollutant = aqiMatch[2].trim();\n}\n\nreturn [{\n  json: {\n    source_b_aqi: aqi,\n    source_b_pollutant: pollutant\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "96355f67-0e9d-4a21-a7cb-3ad678f98e1b",
      "name": "Normalize Open-Meteo Data",
      "type": "n8n-nodes-base.set",
      "position": [
        3056,
        48
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b1",
              "name": "source_a_aqi",
              "type": "number",
              "value": "={{ $json.current.us_aqi }}"
            },
            {
              "id": "b2",
              "name": "source_a_pm25",
              "type": "number",
              "value": "={{ $json.current.pm2_5 }}"
            },
            {
              "id": "b3",
              "name": "source_a_pm10",
              "type": "number",
              "value": "={{ $json.current.pm10 }}"
            },
            {
              "id": "b4",
              "name": "source_a_ozone",
              "type": "number",
              "value": "={{ $json.current.ozone }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d36dcece-3c41-47db-b988-6d656ce5cdf6",
      "name": "Every Day at 630 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        2384,
        48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 6,
              "triggerAtMinute": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "279e1fce-d917-4763-bef0-0cce3a0f1d1c",
      "name": "Fetch AirNow RSS Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3280,
        48
      ],
      "parameters": {
        "url": "={{ $('Set City Configuration').first()?.json.airnow_rss_url || '' }}",
        "options": {}
      },
      "typeVersion": 4.4
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "d52e18ef-32f4-4dfe-8809-a80758d3bb7d",
  "nodeGroups": [],
  "connections": {
    "Gemini AI Model": {
      "ai_languageModel": [
        [
          {
            "node": "Risk Scoring Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "If Alert Needed": {
      "main": [
        [
          {
            "node": "Route by Severity",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Do Nothing on Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Risk Results": {
      "main": [
        [
          {
            "node": "Check Duplicate Alert Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Severity": {
      "main": [
        [
          {
            "node": "Send WhatsApp Moderate Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send WhatsApp Severe Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send WhatsApp Hazardous Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Risk Scoring Agent": {
      "main": [
        [
          {
            "node": "Map Risk Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Day at 630 AM": {
      "main": [
        [
          {
            "node": "Set City Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Open-Meteo AQI": {
      "main": [
        [
          {
            "node": "Normalize Open-Meteo Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch AirNow RSS Data": {
      "main": [
        [
          {
            "node": "Normalize AirNow Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize AirNow Data": {
      "main": [
        [
          {
            "node": "Calculate Weighted Avg AQI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set City Configuration": {
      "main": [
        [
          {
            "node": "Fetch Open-Meteo AQI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set User Health Profile": {
      "main": [
        [
          {
            "node": "Prepare Risk Scoring Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Open-Meteo Data": {
      "main": [
        [
          {
            "node": "Fetch AirNow RSS Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Risk Scoring Data": {
      "main": [
        [
          {
            "node": "Risk Scoring Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Weighted Avg AQI": {
      "main": [
        [
          {
            "node": "Set User Health Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send WhatsApp Severe Alert": {
      "main": [
        [
          {
            "node": "Send Twilio SMS Severe Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Risk Output Structure": {
      "ai_outputParser": [
        [
          {
            "node": "Risk Scoring Agent",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Check Duplicate Alert Status": {
      "main": [
        [
          {
            "node": "If Alert Needed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Twilio SMS Severe Alert": {
      "main": [
        [
          {
            "node": "Post Severe Alert to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send WhatsApp Hazardous Alert": {
      "main": [
        [
          {
            "node": "Send Twilio SMS Hazardous Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Twilio SMS Hazardous Alert": {
      "main": [
        [
          {
            "node": "Post Urgent Alert to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}