{
  "id": "5AETw7H21XDd5RX2",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Rebook passengers automatically after flight cancellations or delays",
  "tags": [],
  "nodes": [
    {
      "id": "c78dc4d4-b79c-4733-92bb-cbffa4e2e819",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        480,
        304
      ],
      "parameters": {
        "width": 1040,
        "height": 1408,
        "content": "## IROPS Auto-Rebooking Agent\n\nThis workflow continuously polls live flight status against active bookings, detects Irregular Operations (IROPS) events (cancellations and severe delays), uses AI to pick the best alternative flight per passenger and draft a calm, personal notification, writes every rebooking decision to a log, and texts the passenger before they ever have to call.\n\n### Who's it for\n- Airlines and OTAs that want autonomous rebooking instead of call-center queues during IROPS\n- Corporate travel desks monitoring VIP/loyalty travelers across many flights\n- Travel agencies that need to proactively protect client itineraries\n- Anyone who wants passengers rebooked and notified in minutes, not hours\n\n### How it works\n1. A schedule trigger polls a live flight-status feed and the active bookings list (both modeled as Google Sheets, swap for a real GDS/airline ops API)\n2. Flight status is matched against each active booking to compute an impact level: On Time, Minor Delay, Severe Delay, or Cancelled\n3. Every booking's status is logged to the OpsLog tab on every run, building a continuous IROPS audit trail regardless of impact\n4. Bookings that are Cancelled or Severely Delayed are filtered out for rebooking action\n5. Alternate flight inventory on the same route is fetched and narrowed to seats-available options in the passenger's cabin class\n6. AI selects the best alternative (or explains why none exists yet) and writes a short, calm SMS notification plus the rebooking reason\n7. The rebooking decision is appended to the Rebookings log tab\n8. The passenger receives an SMS confirming their new flight before they need to call in\n\n### How to set up\n1. Import this workflow into n8n\n2. Add Anthropic Claude credentials and attach to the Anthropic model node\n3. Add Twilio credentials (HTTP Basic Auth, using your Twilio Account SID and Auth Token) and attach them to the Send SMS node; replace YOUR_TWILIO_ACCOUNT_SID and YOUR_TWILIO_PHONE_NUMBER\n4. Switch the Google Sheets HTTP nodes to the native Google Sheets node with OAuth2, or add OAuth2 credentials directly\n5. Replace YOUR_SHEET_ID in all Sheets HTTP nodes with your actual Spreadsheet ID\n6. Set up your Google Sheet with five tabs: FlightStatus, Bookings, Inventory, OpsLog, Rebookings (column layouts below)\n7. Set the schedule interval to match how often your flight-status feed updates (default: every 5 minutes)\n8. Activate the workflow\n\n### Requirements\n- Anthropic Claude API key (claude-sonnet-4 recommended)\n- Twilio account for SMS delivery\n- Google Sheets with OAuth2 credentials (FlightStatus, Bookings, Inventory, OpsLog, Rebookings tabs)\n\n### How to customize\n- Adjust the delay thresholds that define Minor vs Severe delay in JS - Match Bookings & Detect IROPS Impact\n- Add loyalty-tier priority ordering so top-tier travelers get first pick of scarce alternative seats\n- Add an email node alongside SMS for passengers who prefer email, or a Slack node for the ops desk\n- Replace the FlightStatus and Inventory Sheets fetches with a real GDS/airline ops API (e.g. AeroAPI, Sabre, Amadeus)\n- Add a human-approval step (e.g. Slack Approve/Deny) before the SMS goes out for VIP or high-fare bookings\n\n### Google Sheets column layout\n\nFlightStatus tab - row 1 headers:\nFlight Number | Origin | Destination | Scheduled Departure | Status | Delay Minutes | Aircraft | Last Updated\n\nBookings tab - row 1 headers:\nBooking ID | Passenger Name | Phone | Email | Flight Number | Cabin Class | Loyalty Tier | Seat | Status\n\nInventory tab - row 1 headers:\nFlight Number | Origin | Destination | Departure Time | Arrival Time | Seats Available | Cabin Classes Available\n\nOpsLog tab - row 1 headers:\nChecked At | Booking ID | Passenger Name | Flight Number | Flight Status | Delay Minutes | Impact Level | Origin | Destination\n\nRebookings tab - row 1 headers:\nRebooked At | Booking ID | Passenger Name | Original Flight | New Flight | New Departure | Reason | Alternative Offered"
      },
      "typeVersion": 1
    },
    {
      "id": "7a1e226d-40d8-4e34-a15e-6266494e0cfa",
      "name": "Sticky Note - Stage 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        352
      ],
      "parameters": {
        "color": 3,
        "width": 1360,
        "height": 1050,
        "content": "## Stage 1: Data Collection & IROPS Matching\n\nSchedule Trigger polls on a fixed interval (default every 5 minutes). Two parallel fetches pull the latest FlightStatus feed and the currently active Bookings list.\n\nEach fetch is followed by a small JS node that renames the raw Sheets `values` array so the two datasets don't collide once merged.\n\nMerge - Combine Status & Bookings joins both datasets into a single item by position.\n\nJS - Match Bookings & Detect IROPS Impact builds one object per active booking: its matched flight status, delay minutes, and an impactLevel (On Time / Minor Delay / Severe Delay / Cancelled).\n\nSplit Out - Bookings turns that single array into one item per booking so the rest of the workflow can process each passenger independently."
      },
      "typeVersion": 1
    },
    {
      "id": "f4356d84-0f39-4b86-9b00-8f2446affce5",
      "name": "Sticky Note - Stage 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3040,
        1056
      ],
      "parameters": {
        "color": 5,
        "width": 860,
        "height": 680,
        "content": "## Stage 2: Continuous Ops Log\n\nThis branch runs for every booking on every cycle, regardless of impact level - it's what builds the continuous IROPS audit trail.\n\nJS - Format Ops Log Record assembles the row for the OpsLog tab: timestamp, flight status, delay minutes, and impact level.\n\nSave Booking Status to Google Sheets OpsLog appends that row, so the sheet always reflects the latest state across every active booking."
      },
      "typeVersion": 1
    },
    {
      "id": "1819a76b-f04b-401a-ad1c-326d2fcf7642",
      "name": "Sticky Note - Stage 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3040,
        336
      ],
      "parameters": {
        "color": 4,
        "width": 1388,
        "height": 620,
        "content": "## Stage 3: AI Rebooking Decision\n\nFilter - Cancelled or Severe Delay Only lets through only bookings whose flight was cancelled or delayed 120+ minutes, so on-time and mildly delayed passengers never trigger AI calls or rebookings.\n\nA Wait buffer respects API rate limits, then Fetch Alternate Flight Inventory pulls the full route inventory and JS - Filter Inventory for Matching Route narrows it to same-route options with available seats in the passenger's cabin class, sorted by departure time.\n\nThe rebooking AI (claude-sonnet-4, temperature 0.2 for consistent decisions) picks the best alternative flight, explains the reason, and writes a short SMS-ready notification.\n\nJS - Parse Rebooking Output extracts the structured JSON safely, with a fallback that auto-selects the earliest available candidate flight (or explains that none exist yet) if AI parsing fails."
      },
      "typeVersion": 1
    },
    {
      "id": "397988c6-ce7b-479f-9966-a300ed84a99b",
      "name": "Sticky Note - Stage 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4464,
        336
      ],
      "parameters": {
        "color": 6,
        "width": 1080,
        "height": 620,
        "content": "## Stage 4: Rebooking Log & Notification\n\nJS - Format Rebooking Log Record assembles the row for the Rebookings tab: original flight, new flight, new departure time, reason, and whether an alternative was offered.\n\nWrite Rebooking to Google Sheets Log appends that row, keeping a permanent record of every autonomous rebooking decision.\n\nSend SMS Alert to Passenger delivers the new flight details and reason to the passenger's phone via Twilio - reaching them before they ever have to call in."
      },
      "typeVersion": 1
    },
    {
      "id": "1a02e4ff-51ba-4221-8b47-22521801f0a9",
      "name": "Schedule Trigger - Poll Flight Status",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1680,
        896
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "68f5de0c-8117-46e5-ac0b-b1ab6d9d35fa",
      "name": "Fetch Live Flight Status Feed",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1904,
        784
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/FlightStatus!A1:H500",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d45fc825-85f8-4abb-a165-86be9098b127",
      "name": "JS - Rename Flight Status Values",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        784
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nreturn {\n  json: {\n    flightStatusRows: item.values || []\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "c4510dec-bdf6-457e-b9d5-257637cf138b",
      "name": "Fetch Active Bookings",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1904,
        1088
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Bookings!A1:I1000",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "04d665b5-e620-40e1-a9b0-3ef6b513ceb4",
      "name": "JS - Rename Booking Values",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        1088
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nreturn {\n  json: {\n    bookingRows: item.values || []\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "30ff8257-fd94-4348-930d-e8434fa438ff",
      "name": "Merge - Combine Status & Bookings",
      "type": "n8n-nodes-base.merge",
      "position": [
        2352,
        944
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3.2
    },
    {
      "id": "d6e1eaf4-c509-4f0f-95c9-07f52940415f",
      "name": "JS - Match Bookings & Detect IROPS Impact",
      "type": "n8n-nodes-base.code",
      "position": [
        2576,
        944
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst statusRows = (item.flightStatusRows || []).slice(1); // skip header\nconst bookingRows = (item.bookingRows || []).slice(1); // skip header\n\n// FlightStatus columns: [flightNumber, origin, destination, scheduledDeparture, status, delayMinutes, aircraft, lastUpdated]\n// Bookings columns: [bookingId, passengerName, phone, email, flightNumber, cabinClass, loyaltyTier, seat, status]\n\nconst statusByFlight = {};\nstatusRows.forEach(r => {\n  statusByFlight[r[0]] = {\n    flightNumber: r[0] || '',\n    origin: r[1] || '',\n    destination: r[2] || '',\n    scheduledDeparture: r[3] || '',\n    status: r[4] || 'Unknown',\n    delayMinutes: parseInt(r[5], 10) || 0,\n    aircraft: r[6] || '',\n    lastUpdated: r[7] || ''\n  };\n});\n\nconst bookings = bookingRows\n  .filter(r => (r[8] || '').toLowerCase() === 'active')\n  .map(r => {\n    const flightNumber = r[4] || '';\n    const flight = statusByFlight[flightNumber] || {};\n    const status = flight.status || 'Unknown';\n    const delayMinutes = flight.delayMinutes || 0;\n\n    let impactLevel = 'On Time';\n    if (status.toLowerCase() === 'cancelled') impactLevel = 'Cancelled';\n    else if (delayMinutes >= 120) impactLevel = 'Severe Delay';\n    else if (delayMinutes >= 45) impactLevel = 'Minor Delay';\n\n    return {\n      bookingId: r[0] || '',\n      passengerName: r[1] || '',\n      phone: r[2] || '',\n      email: r[3] || '',\n      flightNumber,\n      cabinClass: r[5] || '',\n      loyaltyTier: r[6] || '',\n      seat: r[7] || '',\n      origin: flight.origin || '',\n      destination: flight.destination || '',\n      scheduledDeparture: flight.scheduledDeparture || '',\n      flightStatus: status,\n      delayMinutes,\n      impactLevel\n    };\n  });\n\nreturn { json: { bookings, checkedAt: new Date().toISOString() } };"
      },
      "typeVersion": 2
    },
    {
      "id": "4c3bb359-20a4-465c-b4cc-a1e84ef5655a",
      "name": "Split Out - Bookings",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        2800,
        944
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "bookings"
      },
      "typeVersion": 1
    },
    {
      "id": "25398099-aa29-4099-b448-73ae9e554765",
      "name": "JS - Format Ops Log Record",
      "type": "n8n-nodes-base.code",
      "position": [
        3088,
        1328
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst loggedAt = new Date().toISOString();\n\nreturn {\n  json: {\n    ...item,\n    loggedAt,\n    opsLogRow: [\n      loggedAt,\n      item.bookingId || '',\n      item.passengerName || '',\n      item.flightNumber || '',\n      item.flightStatus || '',\n      item.delayMinutes || 0,\n      item.impactLevel || '',\n      item.origin || '',\n      item.destination || ''\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "bb5e954a-6edf-4ef2-8026-770d0bf9c6ea",
      "name": "Wait For Limit",
      "type": "n8n-nodes-base.wait",
      "position": [
        3296,
        1328
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "36658495-1c7e-4afe-9ffb-6185150736c0",
      "name": "Save Booking Status to Google Sheets OpsLog",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3552,
        1328
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/OpsLog!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"values\": [{{ JSON.stringify($json.opsLogRow) }}]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b02f6b8f-2719-4a3d-96f8-dc475e70a028",
      "name": "Filter - Cancelled or Severe Delay Only",
      "type": "n8n-nodes-base.filter",
      "position": [
        3088,
        688
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ ['Cancelled','Severe Delay'].includes($json.impactLevel) }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "751f6ef7-a022-4b3b-a1e3-c7c46e1ade5b",
      "name": "Wait - API Rate Limit",
      "type": "n8n-nodes-base.wait",
      "position": [
        3280,
        672
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "72afc645-1173-4000-a3a1-a965f5b283a9",
      "name": "Fetch Alternate Flight Inventory",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3504,
        672
      ],
      "parameters": {
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Inventory!A1:G1000",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "2c94e819-ffe2-414a-acf1-3209c7369efa",
      "name": "JS - Filter Inventory for Matching Route",
      "type": "n8n-nodes-base.code",
      "position": [
        3728,
        672
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst invRows = (item.values || []).slice(1); // skip header\n\n// Inventory columns: [flightNumber, origin, destination, departureTime, arrivalTime, seatsAvailable, cabinClassesAvailable]\n\nconst candidateFlights = invRows\n  .filter(r =>\n    r[1] === item.origin &&\n    r[2] === item.destination &&\n    r[0] !== item.flightNumber &&\n    (parseInt(r[5], 10) || 0) > 0 &&\n    (r[6] || '').toLowerCase().includes((item.cabinClass || '').toLowerCase())\n  )\n  .map(r => ({\n    flightNumber: r[0] || '',\n    origin: r[1] || '',\n    destination: r[2] || '',\n    departureTime: r[3] || '',\n    arrivalTime: r[4] || '',\n    seatsAvailable: parseInt(r[5], 10) || 0,\n    cabinClassesAvailable: r[6] || ''\n  }))\n  .sort((a, b) => new Date(a.departureTime) - new Date(b.departureTime))\n  .slice(0, 5);\n\nreturn { json: { ...item, candidateFlights } };"
      },
      "typeVersion": 2
    },
    {
      "id": "090a5953-87d8-4cd7-b9e4-1455b7fbe8f7",
      "name": "AI - Generate Rebooking Decision",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        3952,
        672
      ],
      "parameters": {
        "text": "=You are an airline IROPS (irregular operations) rebooking specialist.\n\nPassenger: {{ $json.passengerName }} (Booking {{ $json.bookingId }})\nLoyalty Tier: {{ $json.loyaltyTier }}\nCabin Class: {{ $json.cabinClass }}\nOriginal Flight: {{ $json.flightNumber }} ({{ $json.origin }} to {{ $json.destination }})\nScheduled Departure: {{ $json.scheduledDeparture }}\nFlight Status: {{ $json.flightStatus }} (Impact: {{ $json.impactLevel }}, Delay: {{ $json.delayMinutes }} minutes)\n\nCandidate Alternative Flights (same route, seats available, matching cabin class):\n{{ JSON.stringify($json.candidateFlights) }}\n\nSelect the single best alternative flight for this passenger, prioritizing the earliest reasonable departure that protects their cabin class. If no candidates are available, say so honestly rather than inventing one.\n\nWrite a short, calm, reassuring notification suitable for SMS that tells the passenger what happened and what their new flight is (or that the team is actively working on options if none exist yet).\n\nReturn a JSON object with EXACTLY these fields:\n{\n  \"selectedFlightNumber\": \"Flight number from the candidates, or empty string if none available\",\n  \"selectedDepartureTime\": \"Departure time of the selected flight, or empty string if none available\",\n  \"rebookingReason\": \"One sentence explaining the rebooking decision\",\n  \"notificationMessage\": \"Under 200 characters, suitable for SMS\",\n  \"alternativeOffered\": true or false\n}\n\nReturn ONLY the JSON. No markdown, no explanation.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "eb0a9402-8a04-46f7-a2fd-fd4fa77053bb",
      "name": "Anthropic - Rebooking Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        3952,
        896
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-20250514"
        },
        "options": {
          "temperature": 0.2
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "2c03c44f-8df3-463c-809d-3481f582ae8a",
      "name": "JS - Parse Rebooking Output",
      "type": "n8n-nodes-base.code",
      "position": [
        4480,
        672
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nlet parsed = {};\ntry {\n  const rawText = item.output || item.text || item.response || '{}';\n  const clean = rawText.replace(/```json|```/g, '').trim();\n  parsed = JSON.parse(clean);\n} catch (e) {\n  const alt = (item.candidateFlights && item.candidateFlights[0]) || null;\n  if (alt) {\n    parsed = {\n      selectedFlightNumber: alt.flightNumber,\n      selectedDepartureTime: alt.departureTime,\n      rebookingReason: `Automatic rebooking due to ${(item.impactLevel || '').toLowerCase()} on ${item.flightNumber}.`,\n      notificationMessage: `Your flight ${item.flightNumber} was affected (${item.impactLevel}). We've rebooked you on flight ${alt.flightNumber} departing ${alt.departureTime}.`,\n      alternativeOffered: true\n    };\n  } else {\n    parsed = {\n      selectedFlightNumber: '',\n      selectedDepartureTime: '',\n      rebookingReason: `No alternative flights currently available for ${item.flightNumber}.`,\n      notificationMessage: `Your flight ${item.flightNumber} was affected (${item.impactLevel}). Our team is finding you the next available option and will update you shortly.`,\n      alternativeOffered: false\n    };\n  }\n}\nreturn {\n  json: {\n    ...item,\n    ...parsed\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "cfc2357a-8b9a-48ea-bffa-bd24e396a067",
      "name": "JS - Format Rebooking Log Record",
      "type": "n8n-nodes-base.code",
      "position": [
        4672,
        672
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const item = $input.item.json;\nconst rebookedAt = new Date().toISOString();\n\nreturn {\n  json: {\n    ...item,\n    rebookedAt,\n    rebookingLogRow: [\n      rebookedAt,\n      item.bookingId || '',\n      item.passengerName || '',\n      item.flightNumber || '',\n      item.selectedFlightNumber || '',\n      item.selectedDepartureTime || '',\n      item.rebookingReason || '',\n      item.alternativeOffered ? 'Yes' : 'No'\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "8637fbcf-09b9-4dda-8b37-c2e39906c052",
      "name": "Write Rebooking to Google Sheets Log",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5008,
        672
      ],
      "parameters": {
        "url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Rebookings!A1:append?valueInputOption=USER_ENTERED",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"values\": [{{ JSON.stringify($json.rebookingLogRow) }}]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "oAuth2Api"
      },
      "credentials": {
        "oAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "91e96526-6bbe-4d27-8941-5b1026e90131",
      "name": "Send SMS Alert to Passenger",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        5232,
        672
      ],
      "parameters": {
        "url": "=https://api.twilio.com/2010-04-01/Accounts/YOUR_TWILIO_ACCOUNT_SID/Messages.json",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"To\": \"{{ $json.phone }}\",\n  \"From\": \"YOUR_TWILIO_PHONE_NUMBER\",\n  \"Body\": \"{{ $json.notificationMessage }}\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth"
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "c3a26cd3-8deb-465d-bca8-b3552a149dfb",
      "name": "Wait For Result",
      "type": "n8n-nodes-base.wait",
      "position": [
        4272,
        672
      ],
      "parameters": {
        "amount": 15
      },
      "typeVersion": 1.1
    },
    {
      "id": "9a0831b5-517e-44fd-a6df-a8b3d52ac958",
      "name": "Wait For Data",
      "type": "n8n-nodes-base.wait",
      "position": [
        4832,
        672
      ],
      "parameters": {},
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "21eacc00-1c17-449a-be1a-d190a5eea91d",
  "connections": {
    "Wait For Data": {
      "main": [
        [
          {
            "node": "Write Rebooking to Google Sheets Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Limit": {
      "main": [
        [
          {
            "node": "Save Booking Status to Google Sheets OpsLog",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait For Result": {
      "main": [
        [
          {
            "node": "JS - Parse Rebooking Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out - Bookings": {
      "main": [
        [
          {
            "node": "JS - Format Ops Log Record",
            "type": "main",
            "index": 0
          },
          {
            "node": "Filter - Cancelled or Severe Delay Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Active Bookings": {
      "main": [
        [
          {
            "node": "JS - Rename Booking Values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - API Rate Limit": {
      "main": [
        [
          {
            "node": "Fetch Alternate Flight Inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Format Ops Log Record": {
      "main": [
        [
          {
            "node": "Wait For Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Rename Booking Values": {
      "main": [
        [
          {
            "node": "Merge - Combine Status & Bookings",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Anthropic - Rebooking Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Rebooking Decision",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse Rebooking Output": {
      "main": [
        [
          {
            "node": "JS - Format Rebooking Log Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Live Flight Status Feed": {
      "main": [
        [
          {
            "node": "JS - Rename Flight Status Values",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Rebooking Decision": {
      "main": [
        [
          {
            "node": "Wait For Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Alternate Flight Inventory": {
      "main": [
        [
          {
            "node": "JS - Filter Inventory for Matching Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Format Rebooking Log Record": {
      "main": [
        [
          {
            "node": "Wait For Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Rename Flight Status Values": {
      "main": [
        [
          {
            "node": "Merge - Combine Status & Bookings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge - Combine Status & Bookings": {
      "main": [
        [
          {
            "node": "JS - Match Bookings & Detect IROPS Impact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Rebooking to Google Sheets Log": {
      "main": [
        [
          {
            "node": "Send SMS Alert to Passenger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger - Poll Flight Status": {
      "main": [
        [
          {
            "node": "Fetch Live Flight Status Feed",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Active Bookings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter - Cancelled or Severe Delay Only": {
      "main": [
        [
          {
            "node": "Wait - API Rate Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Filter Inventory for Matching Route": {
      "main": [
        [
          {
            "node": "AI - Generate Rebooking Decision",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Match Bookings & Detect IROPS Impact": {
      "main": [
        [
          {
            "node": "Split Out - Bookings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}