{
  "id": "Aa91A2pT4DEUZQ6i",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Predict airport queue wait times and send traveler alerts via email",
  "tags": [],
  "nodes": [
    {
      "id": "4bbe129d-3600-44aa-ab2f-f14c95642267",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -64
      ],
      "parameters": {
        "width": 960,
        "height": 948,
        "content": "## \u2708\ufe0f Airport Queue & Wait-Time Predictor\n\nThis workflow predicts real-time wait times for security, immigration, and boarding queues using live airport data and historical patterns. It recommends optimal arrival times and sends proactive alerts to minimize traveler delays.\n\n### Who's it for\n\u2022 Travelers wanting smart arrival-time guidance\n\u2022 Airport ops teams monitoring queue health\n\u2022 Travel apps needing real-time queue APIs\n\u2022 Airlines proactively alerting at-risk passengers\n\n### How it works\n1. Triggered by traveler request (webhook) or scheduled poll (every 5 min)\n2. Fetches live airport operational data (queues, staffing, flight status)\n3. Wait \u2014 rate-limit buffer before heavy computation\n4. JS engine runs wait-time prediction model + arrival recommendation\n5. AI node generates natural-language traveler alert message\n6. Wait \u2014 review / de-duplication buffer\n7. Sends push notification / SMS alert to traveler\n8. Logs prediction + outcome to Google Sheets tracker\n\n### Setup\n1. Import workflow into n8n\n2. Configure credentials: AviationStack or AeroDataBox API, SendGrid / Twilio, Google Sheets OAuth2\n3. Set YOUR_SHEET_ID in tracker node\n4. Set ALERT_EMAIL / ALERT_PHONE in notification nodes\n5. Activate workflow\n\n### Requirements\n\u2022 Airport data API (AviationStack / AeroDataBox / FlightAware)\n\u2022 SendGrid (email) or Twilio (SMS) for alerts\n\u2022 Google Sheets OAuth2\n\u2022 OpenAI / Anthropic API key\n\n### Customisation\n\u2022 Tune threshold minutes in JS Prediction node\n\u2022 Add extra checkpoints (lounge, gate bus) in Set node\n\u2022 Swap AI model in LLM node\n\u2022 Extend Google Sheet columns for analytics"
      },
      "typeVersion": 1
    },
    {
      "id": "a732f4aa-d2a5-46b3-b669-ccaaaa7d7089",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1056,
        80
      ],
      "parameters": {
        "color": 6,
        "width": 780,
        "height": 840,
        "content": "## 1. Trigger & Data Ingestion\n\nTwo entry points:\n\u2022 Webhook \u2192 traveler submits flight details in real time\n\u2022 Scheduler \u2192 polls airport data every 5 minutes for proactive alerts\n\nSet node normalises both payloads into a unified context object before downstream processing."
      },
      "typeVersion": 1
    },
    {
      "id": "3ba21e52-dedc-4837-a134-12d888ecdcf8",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1888,
        16
      ],
      "parameters": {
        "color": 6,
        "width": 740,
        "height": 908,
        "content": "## 2. Prediction & AI Recommendation\n\n\u2022 Wait 1 throttles API calls before fetching live queue data\n\u2022 JS Code node runs the full prediction model:\n  \u2013 Historical baseline lookup (hour-of-day \u00d7 day-of-week matrix)\n  \u2013 Live queue multiplier from ingested data\n  \u2013 Seasonal adjustment factor\n  \u2013 Optimal arrival window calculation\n  \u2013 Risk flag: willMissFlight boolean\n\u2022 AI Agent node converts raw prediction into a friendly, actionable traveler message"
      },
      "typeVersion": 1
    },
    {
      "id": "a42ba0c0-b8ce-421c-b4f7-e2427a8667fb",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2672,
        224
      ],
      "parameters": {
        "color": 6,
        "width": 980,
        "height": 580,
        "content": "## 3. Alert Delivery & Tracking\n\n\u2022 HTTP node fires SMS via Twilio OR email via SendGrid\n\u2022 HTTP node appends prediction row to Google Sheets for analytics\n\u2022 Both outputs run in parallel after Wait 2"
      },
      "typeVersion": 1
    },
    {
      "id": "c76c6fc9-b5e4-4cd8-a50e-fabca6dfe509",
      "name": "Webhook - Traveler Queue Request",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1232,
        384
      ],
      "parameters": {
        "path": "airport-queue-check",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 1.1
    },
    {
      "id": "f064b52c-af1f-4ead-84f3-9b7e8588278f",
      "name": "Poll Airport Data Every 5 Min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1232,
        592
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/5 * * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "858bdd5a-09e8-4b98-801c-049df33e13f4",
      "name": "Normalise Traveler & Airport Context",
      "type": "n8n-nodes-base.set",
      "position": [
        1488,
        480
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "flightNumber",
              "type": "string",
              "value": "={{ $json.flightNumber || $json.body?.flightNumber || 'UNKNOWN' }}"
            },
            {
              "name": "airportCode",
              "type": "string",
              "value": "={{ $json.airportCode || $json.body?.airportCode || 'DEL' }}"
            },
            {
              "name": "terminal",
              "type": "string",
              "value": "={{ $json.terminal || $json.body?.terminal || 'T2' }}"
            },
            {
              "name": "departureTime",
              "type": "string",
              "value": "={{ $json.departureTime || $json.body?.departureTime || new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString() }}"
            },
            {
              "name": "travelerEmail",
              "type": "string",
              "value": "={{ $json.travelerEmail || $json.body?.travelerEmail || '' }}"
            },
            {
              "name": "travelerPhone",
              "type": "string",
              "value": "={{ $json.travelerPhone || $json.body?.travelerPhone || '' }}"
            },
            {
              "name": "passengerType",
              "type": "string",
              "value": "={{ $json.passengerType || $json.body?.passengerType || 'standard' }}"
            },
            {
              "name": "requestId",
              "type": "string",
              "value": "={{ $json.requestId || 'REQ-' + Date.now().toString() }}"
            },
            {
              "name": "liveQueueData",
              "type": "object",
              "value": "={{ $json.liveQueueData || $json.body?.liveQueueData || { security: { queueLength: 45, staffCount: 6, avgProcessTimeSec: 38 }, immigration: { queueLength: 30, staffCount: 4, avgProcessTimeSec: 55 }, boarding: { queueLength: 20, gateOpen: true, boardingStartsMin: 40 } } }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "1c3ca75e-1c55-49de-a111-d2eb70bf793e",
      "name": "Wait 1 - API Rate Limit Buffer",
      "type": "n8n-nodes-base.wait",
      "position": [
        1728,
        480
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a134d45d-9876-420a-a62f-dca0b31e041a",
      "name": "JS - Wait-Time Prediction Engine",
      "type": "n8n-nodes-base.code",
      "position": [
        2016,
        480
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ============================================================\n// AIRPORT QUEUE WAIT-TIME PREDICTION ENGINE\n// ============================================================\nconst item = $input.item.json;\n\n// --- 1. Historical baseline matrix (minutes) per checkpoint\n//        Rows = hour of day (0-23), values = [security, immigration, boarding]\nconst HISTORICAL_BASELINE = {\n  security:    [5,4,4,3,3,4,8,18,26,22,20,18,22,24,21,19,23,28,25,20,16,12,9,6],\n  immigration: [3,3,2,2,2,3,5,12,20,18,16,15,18,20,17,15,19,22,20,16,12,9,6,4],\n  boarding:    [2,2,2,1,1,2,4,8,12,10,9,9,11,12,10,9,11,13,12,10,8,6,4,3]\n};\n\n// --- 2. Day-of-week multiplier (0=Sun \u2026 6=Sat)\nconst DOW_MULTIPLIER = [1.15, 1.05, 1.0, 1.0, 1.05, 1.35, 1.40];\n\n// --- 3. Seasonal multiplier (month 0-indexed)\nconst SEASONAL_MULTIPLIER = [1.10,1.05,1.0,1.0,1.05,1.20,1.35,1.35,1.10,1.0,1.05,1.25];\n\n// --- 4. Parse inputs\nconst now = new Date();\nconst hour = now.getHours();\nconst dow  = now.getDay();\nconst mon  = now.getMonth();\n\nconst departureTime = new Date(item.departureTime || now.getTime() + 3 * 3600000);\nconst minutesToDeparture = Math.max(0, (departureTime - now) / 60000);\n\nconst liveData  = item.liveQueueData || {};\nconst security   = liveData.security   || {};\nconst immigration = liveData.immigration || {};\nconst boarding   = liveData.boarding   || {};\n\n// --- 5. Compute live-queue multiplier for each checkpoint\nfunction liveMultiplier(queueLen, staffCount, defaultQ = 20, defaultS = 4) {\n  const q = queueLen  || defaultQ;\n  const s = staffCount || defaultS;\n  return Math.max(0.5, Math.min(3.5, (q / defaultQ) * (defaultS / s)));\n}\n\nconst secLiveMult  = liveMultiplier(security.queueLength,   security.staffCount,   30, 5);\nconst immLiveMult  = liveMultiplier(immigration.queueLength, immigration.staffCount, 25, 4);\nconst brdLiveMult  = liveMultiplier(boarding.queueLength,    4,                      15, 4);\n\n// --- 6. Final predicted wait times (minutes) per checkpoint\nconst dowFactor = DOW_MULTIPLIER[dow]   || 1.0;\nconst seaFactor = SEASONAL_MULTIPLIER[mon] || 1.0;\n\nfunction predict(baseline, liveMult) {\n  return Math.round(baseline * liveMult * dowFactor * seaFactor);\n}\n\nconst secWait  = predict(HISTORICAL_BASELINE.security[hour],   secLiveMult);\nconst immWait  = predict(HISTORICAL_BASELINE.immigration[hour], immLiveMult);\nconst brdWait  = predict(HISTORICAL_BASELINE.boarding[hour],   brdLiveMult);\n\nconst totalWait     = secWait + immWait + brdWait;\nconst bufferMinutes = item.passengerType === 'premium' ? 20 : 35;\nconst recommendedArrivalMin = totalWait + bufferMinutes;\n\n// --- 7. Risk assessment\nconst willMissFlight = minutesToDeparture < (totalWait + 15);\nconst atRisk         = minutesToDeparture < (totalWait + bufferMinutes);\n\n// --- 8. Congestion level label\nfunction congestionLabel(mins) {\n  if (mins <= 10) return 'LOW';\n  if (mins <= 20) return 'MODERATE';\n  if (mins <= 35) return 'HIGH';\n  return 'SEVERE';\n}\n\nconst overallCongestion = congestionLabel(totalWait);\n\n// --- 9. Optimal arrival window\nconst optimalArrivalTime = new Date(departureTime.getTime() - recommendedArrivalMin * 60000);\n\nreturn {\n  json: {\n    ...item,\n    prediction: {\n      security:   { waitMinutes: secWait,  congestion: congestionLabel(secWait)  },\n      immigration: { waitMinutes: immWait, congestion: congestionLabel(immWait) },\n      boarding:   { waitMinutes: brdWait,  congestion: congestionLabel(brdWait)  },\n      totalWaitMinutes:        totalWait,\n      overallCongestion,\n      recommendedArrivalMinutesBeforeFlight: recommendedArrivalMin,\n      optimalArrivalTime:      optimalArrivalTime.toISOString(),\n      minutesToDeparture:      Math.round(minutesToDeparture),\n      riskFlags: {\n        willMissFlight,\n        atRisk,\n        alertRequired: atRisk || willMissFlight\n      },\n      modelMeta: {\n        hourOfDay:          hour,\n        dayOfWeek:          dow,\n        month:              mon,\n        dowMultiplier:      dowFactor,\n        seasonalMultiplier: seaFactor,\n        computedAt:         now.toISOString()\n      }\n    }\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "98793a2e-c804-49df-8c8f-b9fc06202e59",
      "name": "AI - Generate Traveler Alert Message",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2336,
        480
      ],
      "parameters": {
        "text": "=You are a friendly airport concierge AI. Using the structured prediction data below, write a clear, concise, and reassuring traveler alert message (max 120 words). Use plain language, include specific checkpoint wait times, and give a clear action \u2014 what the traveler should do right now.\n\nFlight: {{ $json.flightNumber }} departing {{ $json.departureTime }}\nAirport: {{ $json.airportCode }} Terminal {{ $json.terminal }}\nPassenger type: {{ $json.passengerType }}\n\nPredicted waits:\n\u2022 Security: {{ $json.prediction.security.waitMinutes }} min ({{ $json.prediction.security.congestion }})\n\u2022 Immigration: {{ $json.prediction.immigration.waitMinutes }} min ({{ $json.prediction.immigration.congestion }})\n\u2022 Boarding queue: {{ $json.prediction.boarding.waitMinutes }} min ({{ $json.prediction.boarding.congestion }})\n\u2022 Total queue time: {{ $json.prediction.totalWaitMinutes }} min\n\nRecommended airport arrival: {{ $json.prediction.recommendedArrivalMinutesBeforeFlight }} min before departure ({{ $json.prediction.optimalArrivalTime }})\nMinutes until departure: {{ $json.prediction.minutesToDeparture }}\nAt risk of missing flight: {{ $json.prediction.riskFlags.atRisk }}\nWill miss flight if not acting now: {{ $json.prediction.riskFlags.willMissFlight }}\n\nIf atRisk or willMissFlight is true, open with an urgent but calm warning. Otherwise open with a positive, informative tone. End with one practical tip for this specific terminal.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "ca739c53-6a4e-4b19-ab14-862f6b8a8d0d",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        2080,
        704
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "db4a399c-cddb-4ab0-90e0-66717a0c2f0c",
      "name": "JS - Format Alert Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        2736,
        480
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// ============================================================\n// FORMAT FINAL ALERT PAYLOAD FOR DELIVERY & TRACKING\n// ============================================================\nconst item = $input.item.json;\n\nconst alertText = item.output\n  || item.response\n  || item.text\n  || `\u26a0\ufe0f Your flight ${item.flightNumber} alert: total queue time ~${item.prediction?.totalWaitMinutes ?? '?'} min. Arrive by ${item.prediction?.optimalArrivalTime ?? 'ASAP'}.`;\n\nconst alertLevel = item.prediction?.riskFlags?.willMissFlight\n  ? 'CRITICAL'\n  : item.prediction?.riskFlags?.atRisk\n  ? 'WARNING'\n  : 'INFO';\n\nconst smsBody = `[${alertLevel}] ${alertText.substring(0, 160)}`;\n\nconst emailSubject = alertLevel === 'CRITICAL'\n  ? `\ud83d\udea8 Act Now: Flight ${item.flightNumber} - You May Miss Your Flight`\n  : alertLevel === 'WARNING'\n  ? `\u26a0\ufe0f Heads Up: Queue Alert for Flight ${item.flightNumber}`\n  : `\u2708\ufe0f Queue Update: Flight ${item.flightNumber} - ${item.prediction?.overallCongestion ?? ''} Congestion`;\n\nconst sheetRow = [\n  new Date().toISOString().split('T')[0],\n  item.requestId,\n  item.flightNumber,\n  item.airportCode,\n  item.terminal,\n  item.departureTime,\n  item.prediction?.security?.waitMinutes ?? '',\n  item.prediction?.immigration?.waitMinutes ?? '',\n  item.prediction?.boarding?.waitMinutes ?? '',\n  item.prediction?.totalWaitMinutes ?? '',\n  item.prediction?.overallCongestion ?? '',\n  item.prediction?.recommendedArrivalMinutesBeforeFlight ?? '',\n  item.prediction?.optimalArrivalTime ?? '',\n  alertLevel,\n  new Date().toISOString()\n];\n\nreturn {\n  json: {\n    ...item,\n    alertText,\n    alertLevel,\n    smsBody,\n    emailSubject,\n    sheetRow,\n    deliveryStatus: 'PENDING'\n  }\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7ab9ce86-9293-4306-b9d5-581410fc0c2d",
      "name": "Send Email Alert via SendGrid",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3008,
        368
      ],
      "parameters": {
        "url": "https://api.sendgrid.com/v3/mail/send",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"personalizations\": [{\"to\": [{\"email\": \"{{ $json.travelerEmail }}\"}]}],\n  \"from\": {\"email\": \"alerts@yourairportapp.com\", \"name\": \"Airport Queue Predictor\"},\n  \"subject\": \"{{ $json.emailSubject }}\",\n  \"content\": [{\"type\": \"text/plain\", \"value\": \"{{ $json.alertText }}\"}]\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_SENDGRID_API_KEY"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "532be173-e750-4629-9e28-ddbc74413164",
      "name": "Log Prediction to Google Sheets",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3008,
        608
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Predictions!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        },
        "jsonBody": "={\n  \"values\": [{{ JSON.stringify($json.sheetRow) }}]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api"
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "b0b1d24a-a2af-4085-8824-8597fee23a62",
  "connections": {
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Traveler Alert Message",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "JS - Format Alert Payload": {
      "main": [
        [
          {
            "node": "Send Email Alert via SendGrid",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Prediction to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Airport Data Every 5 Min": {
      "main": [
        [
          {
            "node": "Normalise Traveler & Airport Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1 - API Rate Limit Buffer": {
      "main": [
        [
          {
            "node": "JS - Wait-Time Prediction Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Wait-Time Prediction Engine": {
      "main": [
        [
          {
            "node": "AI - Generate Traveler Alert Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Traveler Queue Request": {
      "main": [
        [
          {
            "node": "Normalise Traveler & Airport Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Traveler Alert Message": {
      "main": [
        [
          {
            "node": "JS - Format Alert Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalise Traveler & Airport Context": {
      "main": [
        [
          {
            "node": "Wait 1 - API Rate Limit Buffer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}