This workflow corresponds to n8n.io template #6235 — we link there as the canonical source.
This workflow follows the Gmail → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "4vQkJTdJPHnD0XUa",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Live Flight Fare Tracker \u2013 Instant Price Drop Alerts via SMS & Email",
"tags": [],
"nodes": [
{
"id": "aa5f054a-6239-46c1-8ab3-492796e1f1c1",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.cron",
"position": [
-1440,
380
],
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "everyX",
"unit": "minutes",
"value": 15
}
]
}
},
"typeVersion": 1
},
{
"id": "8d308fdf-5c40-46ab-b695-a7c95710ada2",
"name": "Fetch Flight Data",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1220,
380
],
"parameters": {
"url": "https://api.aviationstack.com/v1/flights",
"options": {},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpQueryAuth",
"queryParameters": {
"parameters": [
{
"name": "access_key",
"value": "0987c6845c09876yt"
},
{
"name": "dep_iata",
"value": "JFK"
},
{
"name": "arr_iata",
"value": "LAX"
},
{
"name": "limit",
"value": "10"
}
]
}
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "f4675a74-c27d-4b8b-bb8e-624687125b4a",
"name": "Process Flight Data",
"type": "n8n-nodes-base.function",
"position": [
-1000,
380
],
"parameters": {
"functionCode": "// Process flight data and check for fare changes\nconst currentFlights = items[0].json.data || [];\nconst processedFlights = [];\n\nfor (const flight of currentFlights) {\n if (flight.flight_status === 'scheduled' && flight.departure) {\n // Extract fare information (you may need to adapt based on API response)\n const flightInfo = {\n flight_number: flight.flight.iata,\n airline: flight.airline.name,\n departure: flight.departure.airport,\n arrival: flight.arrival.airport,\n departure_time: flight.departure.scheduled,\n arrival_time: flight.arrival.scheduled,\n // Mock fare data - replace with actual fare from your chosen API\n current_fare: Math.floor(Math.random() * 500) + 200,\n route: `${flight.departure.iata}-${flight.arrival.iata}`,\n timestamp: new Date().toISOString()\n };\n \n processedFlights.push(flightInfo);\n }\n}\n\nreturn processedFlights.map(flight => ({ json: flight }));"
},
"executeOnce": true,
"typeVersion": 1
},
{
"id": "87b6bde3-fe0a-47a2-9410-ddb8d78cbf77",
"name": "Check if Alert Needed",
"type": "n8n-nodes-base.if",
"position": [
-340,
380
],
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.fare_change }}",
"operation": "notEqual"
}
]
}
},
"typeVersion": 1
},
{
"id": "11309f53-cdc0-4cf9-b060-d55ca3e9ca74",
"name": "Format Alert Message",
"type": "n8n-nodes-base.function",
"position": [
-80,
340
],
"parameters": {
"functionCode": "// Format alert message\nconst flight = items[0].json;\nconst alertType = flight.alert_type;\nconst emoji = alertType === 'PRICE_DROP' ? '\ud83d\udcc9' : '\ud83d\udcc8';\nconst alertColor = alertType === 'PRICE_DROP' ? 'good' : 'warning';\n\nconst message = {\n email: {\n subject: `${emoji} Flight Fare Alert: ${flight.flight_number}`,\n html: `\n <h2>${emoji} Fare ${alertType.replace('_', ' ')} Alert</h2>\n <p><strong>Flight:</strong> ${flight.flight_number} (${flight.airline})</p>\n <p><strong>Route:</strong> ${flight.departure} \u2192 ${flight.arrival}</p>\n <p><strong>Departure:</strong> ${new Date(flight.departure_time).toLocaleString()}</p>\n <p><strong>Previous Fare:</strong> $${flight.previous_fare}</p>\n <p><strong>Current Fare:</strong> $${flight.current_fare}</p>\n <p><strong>Change:</strong> $${flight.fare_change} (${flight.percentage_change}%)</p>\n <p style=\"color: ${alertType === 'PRICE_DROP' ? 'green' : 'red'};\"><strong>Recommendation:</strong> ${alertType === 'PRICE_DROP' ? 'Consider booking now!' : 'Price increased - monitor for drops'}</p>\n `\n },\n slack: {\n text: `${emoji} Flight Fare Alert`,\n attachments: [\n {\n color: alertColor,\n fields: [\n {\n title: \"Flight\",\n value: `${flight.flight_number} (${flight.airline})`,\n short: true\n },\n {\n title: \"Route\",\n value: `${flight.departure} \u2192 ${flight.arrival}`,\n short: true\n },\n {\n title: \"Previous Fare\",\n value: `$${flight.previous_fare}`,\n short: true\n },\n {\n title: \"Current Fare\",\n value: `$${flight.current_fare}`,\n short: true\n },\n {\n title: \"Change\",\n value: `$${flight.fare_change} (${flight.percentage_change}%)`,\n short: false\n }\n ]\n }\n ]\n },\n sms: `${emoji} FARE ALERT: ${flight.flight_number} ${flight.departure}-${flight.arrival} fare changed from $${flight.previous_fare} to $${flight.current_fare} (${flight.percentage_change}%)`\n};\n\nreturn [{ json: { ...flight, formatted_messages: message } }];"
},
"typeVersion": 1
},
{
"id": "026afcab-023a-4ef4-9573-28ba32edc01c",
"name": "Log Alert Activity",
"type": "n8n-nodes-base.function",
"position": [
460,
400
],
"parameters": {
"functionCode": "// Log alert activity\nconst alert = items[0].json;\n\nconst logEntry = {\n timestamp: new Date().toISOString(),\n flight_number: alert.flight_number,\n route: alert.route,\n alert_type: alert.alert_type,\n fare_change: alert.fare_change,\n percentage_change: alert.percentage_change,\n notification_sent: true\n};\n\nconsole.log('Fare Alert Sent:', logEntry);\n\nreturn [{ json: logEntry }];"
},
"typeVersion": 1
},
{
"id": "374b1e8e-15b3-4109-9c46-01406c0e4ca5",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
-580,
380
],
"parameters": {
"jsCode": "// Get current flight data from previous node (Process Flight Data)\nconst currentFlightsItems = $('Process Flight Data').all();\n\n// Get stored fare data from Google Sheets node\nconst sheetsData = $('Previous flight data').all();\n\nconsole.log('Current flights items:', currentFlightsItems.length);\nconsole.log('Sheets data items:', sheetsData.length);\n\n// Build lookup object from Google Sheets data (remove duplicates)\nconst storedFares = {};\nconst uniqueSheetRows = new Map();\n\n// Remove duplicates from sheets data first\nfor (const row of sheetsData) {\n const rowData = row.json;\n if (rowData.flight_number && rowData.route) {\n const routeKey = `${rowData.flight_number}_${rowData.route}`;\n \n // Keep only the first occurrence of each flight\n if (!uniqueSheetRows.has(routeKey)) {\n uniqueSheetRows.set(routeKey, rowData);\n storedFares[routeKey] = parseFloat(rowData.current_fare || 0);\n }\n }\n}\n\nconsole.log('Stored fares lookup:', Object.keys(storedFares));\n\nconst alertFlights = [];\nconst fareUpdates = [];\n\n// Process each current flight item\nfor (const flightItem of currentFlightsItems) {\n const flightData = flightItem.json;\n \n if (!flightData.flight_number || !flightData.route) {\n console.log('Skipping invalid flight data:', flightData);\n continue;\n }\n \n const routeKey = `${flightData.flight_number}_${flightData.route}`;\n const currentFare = parseFloat(flightData.current_fare || 0);\n const previousFare = storedFares[routeKey];\n \n console.log(`Processing ${routeKey}: Current=${currentFare}, Previous=${previousFare}`);\n \n if (previousFare && previousFare !== currentFare && currentFare > 0) {\n const fareChange = currentFare - previousFare;\n const percentageChange = (fareChange / previousFare) * 100;\n \n console.log(`${routeKey}: Change=${fareChange}, Percentage=${percentageChange.toFixed(2)}%`);\n \n // Alert if fare decreased by 10% or more, or increased by 15% or more\n if (percentageChange <= -10 || percentageChange >= 15) {\n alertFlights.push({\n ...flightData,\n previous_fare: previousFare,\n fare_change: fareChange,\n percentage_change: Math.round(percentageChange * 100) / 100,\n alert_type: percentageChange < 0 ? 'PRICE_DROP' : 'PRICE_INCREASE',\n alert_message: percentageChange < 0 \n ? `\ud83d\udd25 PRICE DROP: ${Math.abs(percentageChange).toFixed(1)}% decrease!` \n : `\u26a0\ufe0f PRICE INCREASE: ${percentageChange.toFixed(1)}% increase!`\n });\n \n console.log(`ALERT TRIGGERED for ${routeKey}: ${percentageChange < 0 ? 'DROP' : 'INCREASE'} of ${Math.abs(percentageChange).toFixed(2)}%`);\n }\n } else if (!previousFare) {\n console.log(`New flight detected: ${routeKey}`);\n } else if (currentFare <= 0) {\n console.log(`Invalid current fare for ${routeKey}: ${currentFare}`);\n }\n \n // Prepare fare updates for Google Sheets (to update stored fares)\n if (currentFare > 0) {\n fareUpdates.push({\n row_number: uniqueSheetRows.get(routeKey)?.row_number || null,\n flight_number: flightData.flight_number,\n airline: flightData.airline || 'Unknown',\n departure: flightData.departure || 'Unknown',\n arrival: flightData.arrival || 'Unknown',\n departure_time: flightData.departure_time || '',\n arrival_time: flightData.arrival_time || '',\n current_fare: currentFare,\n route: flightData.route,\n timestamp: flightData.timestamp || new Date().toISOString(),\n last_updated: new Date().toISOString()\n });\n }\n}\n\nconsole.log(`Total alerts generated: ${alertFlights.length}`);\nif (alertFlights.length > 0) {\n console.log(`Alerts for flights:`, alertFlights.map(f => f.flight_number));\n}\n\n// Store fare updates in node context for the Google Sheets update node\n$node[\"Code\"].context = { fareUpdates };\n\n// If no alerts, return empty array but still process\nif (alertFlights.length === 0) {\n console.log('No fare alerts triggered');\n return [];\n}\n\n// Return alert flights for notification processing\nreturn alertFlights.map(flight => ({ json: flight }));\n"
},
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "43084297-fd73-45ae-83b8-3e31db490777",
"name": "Telegram",
"type": "n8n-nodes-base.telegram",
"onError": "continueRegularOutput",
"position": [
180,
480
],
"parameters": {
"text": "={{ $json.formatted_messages.sms }}",
"chatId": "123SSHSJNASB",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": false,
"typeVersion": 1.2
},
{
"id": "b2048186-6d7a-4b76-9849-bc694592ce39",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1440,
60
],
"parameters": {
"width": 700,
"height": 200,
"content": "## \u2708\ufe0f Flight Fare Tracker & Alert System\n\nThis workflow is designed to monitor flight prices and send alerts when significant fare changes occur (drops or increases). It automates the process of fetching flight data, comparing current fares against historical records, and notifying users via email or Telegram if an alert condition is met. \ud83d\udcc9\ud83d\udcc8"
},
"typeVersion": 1
},
{
"id": "06f7bb02-8151-43f9-b5c2-9a75555d0199",
"name": "Data Ingestion & Processing",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1400,
560
],
"parameters": {
"color": 4,
"width": 600,
"height": 300,
"content": "### \ud83d\udcca Data Ingestion & Processing\n\n1. **Schedule Trigger**: Starts the workflow at regular intervals.\n2. **Fetch Flight Data**: Retrieves flight information from the AviationStack API, filtering by JFK to LAX route.\n3. **Process Flight Data**: A Function node that extracts and formats relevant flight details (flight number, airline, departure/arrival times, and a *mock* current fare). This prepares data for comparison.\n4. **Google Sheets**: Reads existing flight fare data from a Google Sheet, which acts as a historical record."
},
"typeVersion": 1
},
{
"id": "5b431dd1-1832-4a03-9df0-880dc17d1924",
"name": "Fare Comparison & Alert Logic",
"type": "n8n-nodes-base.stickyNote",
"position": [
-700,
20
],
"parameters": {
"color": 3,
"width": 600,
"height": 280,
"content": "### \ud83d\udd0d Fare Comparison & Alert Logic\n\n1. **Code**: This critical node compares the `current_fare` from the \"Process Flight Data\" node with the `previous_fare` fetched from \"Google Sheets\". It calculates fare changes (absolute and percentage) and determines if an alert is needed based on predefined thresholds (e.g., >=10% drop or >=15% increase).\n2. **Check if Alert Needed**: An If node that checks if the `fare_change` calculated in the Code node is non-zero. If there's a change, it proceeds to format and send alerts."
},
"typeVersion": 1
},
{
"id": "a29a641a-f204-463f-8764-91bfaf753f2d",
"name": "Notification & Logging",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"color": 5,
"width": 600,
"height": 280,
"content": "### \ud83d\udd14 Notification & Logging\n\n1. **Format Alert Message**: Prepares the alert message in different formats (email HTML, SMS) using a Function node, based on the `alert_type` (price drop/increase).\n2. **Gmail**: Sends an email notification with the formatted alert.\n3. **Telegram**: Sends a Telegram message with the formatted alert.\n4. **Log Alert Activity**: A final Function node that logs details about the sent alert, useful for auditing or debugging."
},
"typeVersion": 1
},
{
"id": "cf96cc07-5679-4ba4-86f7-5a2cfec7dae3",
"name": "Previous flight data",
"type": "n8n-nodes-base.googleSheets",
"position": [
-780,
380
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WAwV5oNEaedbi9saba87LTkxTFbBWwWaGxsgCmda1_Q/edit?usp=drivesdk",
"cachedResultName": "fare details change logs"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"executeOnce": false,
"typeVersion": 4.6
},
{
"id": "ba34df18-2c7c-46ee-9fc4-acfacb7b8fb1",
"name": "Send a message",
"type": "n8n-nodes-base.gmail",
"position": [
180,
300
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.formatted_messages.email.html }}",
"options": {},
"subject": "={{ $json.formatted_messages.email.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "93eb556a-c580-4639-9d7d-0b0baaa6cda4",
"connections": {
"Code": {
"main": [
[
{
"node": "Check if Alert Needed",
"type": "main",
"index": 0
}
]
]
},
"Telegram": {
"main": [
[
{
"node": "Log Alert Activity",
"type": "main",
"index": 0
}
]
]
},
"Send a message": {
"main": [
[
{
"node": "Log Alert Activity",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Fetch Flight Data",
"type": "main",
"index": 0
}
]
]
},
"Fetch Flight Data": {
"main": [
[
{
"node": "Process Flight Data",
"type": "main",
"index": 0
}
]
]
},
"Process Flight Data": {
"main": [
[
{
"node": "Previous flight data",
"type": "main",
"index": 0
}
]
]
},
"Format Alert Message": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
},
{
"node": "Telegram",
"type": "main",
"index": 0
}
]
]
},
"Previous flight data": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Check if Alert Needed": {
"main": [
[
{
"node": "Format Alert Message",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleApihttpQueryAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This automated n8n workflow continuously tracks real-time flight fare changes by querying airline APIs (e.g., Amadeus, Skyscanner). It compares new prices with historical fares and sends instant notifications to users when a fare drop is detected. All tracked data is structured…
Source: https://n8n.io/workflows/6235/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Template - SSL Expiry Alert System. Uses googleSheets, scheduleTrigger, httpRequest, stickyNote. Scheduled trigger; 21 nodes.
This workflow is ideal for administrators or IT professionals responsible for monitoring SSL certificates of multiple websites to ensure they do not expire unexpectedly.
url-uptime-monitor. Uses scheduleTrigger, splitOut, googleSheets, summarize. Scheduled trigger; 18 nodes.
MPE Kleinanzeigen Unified (Reminder + Poster). Uses gmail, httpRequest, telegram, googleSheets. Scheduled trigger; 15 nodes.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.