This workflow corresponds to n8n.io template #17830 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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": "JA2y2BpdZzoQqUuf",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Automated Flight Price Tracker for your holiday",
"tags": [],
"nodes": [
{
"id": "9a499210-3c4d-4dd6-b670-77946f2b7469",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
624
],
"parameters": {
"width": 480,
"height": 720,
"content": "## Automated Flight Price Tracker for your holiday\n### How it works\n\n1. The workflow triggers daily on a schedule and sets the search configuration parameters.\n2. It searches Google Flights via an external API and parses the returned results.\n3. It checks if the found price meets the target threshold criteria.\n4. If valid, it compares against previously notified prices to prevent alert spam.\n5. Finally, it sends a Telegram notification and updates the price log sheet.\n\n### Setup steps\n\n- - [ ] Connect your Google Sheets account and configure the spreadsheet for logging prices.\n- - [ ] Set up a Telegram bot and provide the bot token and chat ID credentials.\n- - [ ] Configure the Apify or Google Flights API credentials for flight searching.\n\n### Customization\n\nAdjust the schedule trigger frequency, flight search parameters in the Config node, price thresholds, and Telegram recipient settings as needed."
},
"typeVersion": 1
},
{
"id": "f959024a-68ce-40a2-bb5f-e8da6435b47c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1472,
704
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Trigger and configuration setup\n\nTriggers the workflow daily and configures the flight search parameters (origin, destination, dates)."
},
"typeVersion": 1
},
{
"id": "a423c1f9-3f96-4105-bb57-fcccf2edf497",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1920,
736
],
"parameters": {
"color": 7,
"width": 416,
"height": 304,
"content": "## Search and parse flights\n\nPerforms the API request to search Google Flights and parses the raw response data."
},
"typeVersion": 1
},
{
"id": "2fb994a3-cd1e-422a-8b08-ccba4c993541",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2368,
736
],
"parameters": {
"color": 7,
"width": 416,
"height": 400,
"content": "## Check price threshold\n\nEvaluates whether the flight price is below the target threshold."
},
"typeVersion": 1
},
{
"id": "022877ab-0bf8-41e8-964c-41914d269e10",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2816,
640
],
"parameters": {
"color": 7,
"width": 864,
"height": 288,
"content": "## Evaluate alert necessity\n\nRetrieves the last notified price and checks if alert is warranted."
},
"typeVersion": 1
},
{
"id": "2b9e381d-3713-406f-a6b8-9d56fdc84d21",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
3712,
624
],
"parameters": {
"color": 7,
"width": 416,
"height": 304,
"content": "## Send alert and update log\n\nSends the flight alert via Telegram and updates price logs."
},
"typeVersion": 1
},
{
"id": "fed71737-1593-471b-8df9-f61bfb87c94c",
"name": "Every Day at 7am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1520,
864
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 7 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "5dfc534f-ffd8-4c88-a944-8df5b39e6248",
"name": "Set Search Parameters",
"type": "n8n-nodes-base.set",
"notes": "Single control panel. Note: cabinClass must be 'economy', 'premium-economy', 'business', or 'first' for Apify.",
"position": [
1744,
864
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "c1",
"name": "origin",
"type": "string",
"value": "JFK"
},
{
"id": "c2",
"name": "destination",
"type": "string",
"value": "FCO"
},
{
"id": "c3",
"name": "outboundDate",
"type": "string",
"value": "2026-08-15"
},
{
"id": "c4",
"name": "returnDate",
"type": "string",
"value": "2026-08-25"
},
{
"id": "c5",
"name": "adults",
"type": "number",
"value": 2
},
{
"id": "c6",
"name": "children",
"type": "number",
"value": 0
},
{
"id": "c7",
"name": "infants",
"type": "number",
"value": 0
},
{
"id": "c8",
"name": "cabinClass",
"type": "string",
"value": "economy"
},
{
"id": "c9",
"name": "currency",
"type": "string",
"value": "USD"
},
{
"id": "c10",
"name": "region",
"type": "string",
"value": "US"
},
{
"id": "c11",
"name": "priceThreshold",
"type": "number",
"value": 1600
},
{
"id": "c12",
"name": "reAlertBufferAmount",
"type": "number",
"value": 15
}
]
}
},
"typeVersion": 3.4
},
{
"id": "0c0dfbbd-27b3-40b5-bf51-68575efc2664",
"name": "If Price is Low",
"type": "n8n-nodes-base.if",
"position": [
2416,
864
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "p1",
"operator": {
"type": "number",
"operation": "lte"
},
"leftValue": "={{ $json.lowestPrice }}",
"rightValue": "={{ $('Set Search Parameters').item.json.priceThreshold }}"
},
{
"id": "p2",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.offersFound }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "de42558a-a2de-4f42-b36f-7e6fd4070ea6",
"name": "Read Price Log from Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
2864,
768
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit?usp=drivesdk",
"cachedResultName": "Price Log"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.5,
"alwaysOutputData": true
},
{
"id": "81a702c3-15d7-4cba-82c6-7cf648e8c1f8",
"name": "Prepare Alert Decision",
"type": "n8n-nodes-base.set",
"position": [
3088,
768
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d1",
"name": "lowestPrice",
"type": "number",
"value": "={{ $('Parse Flight Results').item.json.lowestPrice }}"
},
{
"id": "d2",
"name": "bookingLink",
"type": "string",
"value": "={{ $('Parse Flight Results').item.json.bookingLink }}"
},
{
"id": "d3",
"name": "routeKey",
"type": "string",
"value": "={{ $('Parse Flight Results').item.json.routeKey }}"
},
{
"id": "d4",
"name": "shouldAlert",
"type": "boolean",
"value": "={{ $json.LastNotifiedPrice ? ($('Parse Flight Results').item.json.lowestPrice <= (Number($json.LastNotifiedPrice) - Number($('Set Search Parameters').item.json.reAlertBufferAmount))) : true }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "24affc19-9be3-4fbe-857f-cd73d3f2721e",
"name": "If Alert Needed",
"type": "n8n-nodes-base.if",
"position": [
3312,
768
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.shouldAlert }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "ea4b062b-7d57-4501-92fb-450ff70cdccf",
"name": "Send Telegram Notification",
"type": "n8n-nodes-base.telegram",
"position": [
3760,
752
],
"parameters": {
"text": "=\u2708\ufe0f *Flight Deal Alert!* \u2708\ufe0f\n\nGreat news! I found *{{ $('Parse Flight Results').item.json.offersFound }}* flight offers matching your criteria. \n\nThe lowest price is currently:\n\ud83d\udd25 *{{ $('Parse Flight Results').item.json.lowestPrice }} {{ $('Parse Flight Results').item.json.currency }}*\n\n*Best Flight Details:*\n\ud83d\udeeb *Outbound:* {{ $('Parse Flight Results').item.json.bestFlightDetails.outbound.departureTime }}\n\ud83c\udfab *Flight No:* `{{ $('Parse Flight Results').item.json.bestFlightDetails.outbound.flightNumber }}`\n\n\ud83d\udeec *Return:* {{ $('Parse Flight Results').item.json.bestFlightDetails.return.departureTime }}\n\ud83c\udfab *Flight No:* `{{ $('Parse Flight Results').item.json.bestFlightDetails.return.flightNumber }}`\n\n\u2708\ufe0f *Airline:* {{ $('Parse Flight Results').item.json.bestFlightDetails.airlines[0] }}\n\u23f1\ufe0f *Total Duration:* {{ $('Parse Flight Results').item.json.bestFlightDetails.durationMinutes }} mins\n\ud83d\uded1 *Stops:* {{ $('Parse Flight Results').item.json.bestFlightDetails.stops }}\n\n\ud83d\udc49 [Click here to view your flights]({{ $json.bookingLink }})\n\n_Price last checked at: {{ $('Parse Flight Results').item.json.checkedAt }}_",
"chatId": "=enter-your-chat-id-here",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "4bb709c0-e7e4-4ed0-8c98-59201c925e95",
"name": "Update Price Log in Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
3984,
752
],
"parameters": {
"columns": {
"value": {
"Origin": "={{ $('Set Search Parameters').item.json.origin }}",
"status": "notified",
"RouteKey": "={{ $('Parse Flight Results').item.json.routeKey }}",
"ReturnDate": "={{ $('Set Search Parameters').item.json.returnDate }}",
"Destination": "={{ $('Set Search Parameters').item.json.destination }}",
"Lowest_price": "={{ Number($('Parse Flight Results').item.json.lowestPrice).toLocaleString('en-US') }}",
"OutboundDate": "={{ $('Set Search Parameters').item.json.outboundDate }}",
"booking_link": "={{ $('Encode Flight URL').item.json.bookingLink }}",
"LastCheckedAt": "={{$now}}",
"LastNotifiedAt": "={{$now}}",
"LastCheckedPrice": "={{$now}}",
"LastNotifiedPrice": "={{$now}}"
},
"schema": [
{
"id": "RouteKey",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "RouteKey",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Origin",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Origin",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Destination",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Destination",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "OutboundDate",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "OutboundDate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ReturnDate",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "ReturnDate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lowest_price",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Lowest_price",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "booking_link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "booking_link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastCheckedPrice",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastCheckedPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastCheckedAt",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastCheckedAt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastNotifiedPrice",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastNotifiedPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastNotifiedAt",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastNotifiedAt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"RouteKey"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit?usp=drivesdk",
"cachedResultName": "Price Log"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "94595db8-9181-4a89-aec3-39aab49d15fc",
"name": "Post to Google Flights API",
"type": "n8n-nodes-base.httpRequest",
"notes": "Updated to match Apify's specific schema.",
"position": [
1968,
864
],
"parameters": {
"url": "=https://api.apify.com/v2/acts/egaiqTO2BW5b0dJrc/run-sync-get-dataset-items",
"method": "POST",
"options": {
"timeout": 120000
},
"jsonBody": "={\n \"searches\": [\n {\n \"origin\": \"{{ $json.origin }}\",\n \"destination\": \"{{ $json.destination }}\",\n \"departureDate\": \"{{ $json.outboundDate }}\",\n \"returnDate\": \"{{ $json.returnDate }}\"\n }\n ],\n \"adults\": {{ $json.adults }},\n \"children\": {{ $json.children }},\n \"infantsOnLap\": {{ $json.infants }},\n \"cabinClass\": \"{{ $json.cabinClass }}\",\n \"currency\": \"{{ $json.currency }}\"\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "f1671520-7d23-4ca9-94cf-d25e756a5736",
"name": "Parse Flight Results",
"type": "n8n-nodes-base.code",
"position": [
2192,
864
],
"parameters": {
"jsCode": "const results = $input.all();\nconst config = $('Set Search Parameters').first().json;\n\nlet lowestPrice = 999999;\nlet offersFound = results.length;\nlet bestFlight = null;\n\n// Loop over Apify dataset items to find the absolute lowest price flight\nif (offersFound > 0) {\n for (const item of results) {\n const data = item.json;\n \n const priceStr = data.price || \"999999\";\n const price = typeof priceStr === 'number' ? priceStr : parseFloat(priceStr.toString().replace(/[^0-9.]/g, ''));\n \n // If this flight is the cheapest we've seen, save its exact details\n if (price && price < lowestPrice) {\n lowestPrice = price;\n bestFlight = data;\n }\n }\n}\n\nconst routeKey = `${config.origin}-${config.destination}`;\n\nlet bookingQuery = `Flights to ${config.destination} from ${config.origin} on ${config.outboundDate}`;\nif (config.returnDate) {\n bookingQuery += ` through ${config.returnDate}`;\n}\n\nlet bestFlightInfo = null;\n\nif (bestFlight) {\n // Extract airlines (e.g. \"Delta and Air France\")\n const airlines = (bestFlight.airlines || []).join(' and ');\n if (airlines) {\n // Append the airlines to the Google Flights search query!\n // This forces Google Flights to show this specific flight.\n bookingQuery += ` on ${airlines}`;\n }\n \n // Create a readable summary of the exact lowest price flight\n bestFlightInfo = {\n airlines: bestFlight.airlines,\n stops: bestFlight.stops,\n durationMinutes: bestFlight.totalDuration,\n outbound: bestFlight.outbound ? {\n departureTime: bestFlight.outbound.segments[0].departureTime,\n flightNumber: bestFlight.outbound.segments[0].airlineCode + bestFlight.outbound.segments[0].flightNumber.replace(/[^0-9]/g, '')\n } : null,\n return: bestFlight.return ? {\n departureTime: bestFlight.return.segments[0].departureTime,\n flightNumber: bestFlight.return.segments[0].airlineCode + bestFlight.return.segments[0].flightNumber.replace(/[^0-9]/g, '')\n } : null\n };\n}\n\nconst bookingLink = `https://www.google.com/travel/flights?q=${encodeURIComponent(bookingQuery)}`;\n\nreturn [\n {\n json: {\n lowestPrice: lowestPrice === 999999 ? null : lowestPrice,\n offersFound: offersFound,\n currency: config.currency,\n routeKey: routeKey,\n bookingLink: bookingLink, // This will now search for the specific airline\n bestFlightDetails: bestFlightInfo, // This mentions the EXACT flight!\n checkedAt: new Date().toISOString()\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "c5da6dc1-ce61-47af-8250-b76556d33f90",
"name": "Log Result to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
2640,
960
],
"parameters": {
"columns": {
"value": {
"Origin": "={{ $('Set Search Parameters').item.json.origin }}",
"status": "not_notified",
"RouteKey": "={{ $('Parse Flight Results').item.json.routeKey }}",
"ReturnDate": "={{ $('Set Search Parameters').item.json.returnDate }}",
"Destination": "={{ $('Set Search Parameters').item.json.destination }}",
"Lowest_price": "={{ Number($('Parse Flight Results').item.json.lowestPrice).toLocaleString('en-US') }}",
"OutboundDate": "={{ $('Set Search Parameters').item.json.outboundDate }}",
"LastCheckedAt": "={{$now}}",
"LastCheckedPrice": "={{$now}}"
},
"schema": [
{
"id": "RouteKey",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "RouteKey",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Origin",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Origin",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Destination",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Destination",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "OutboundDate",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "OutboundDate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ReturnDate",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "ReturnDate",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Lowest_price",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Lowest_price",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "booking_link",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "booking_link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastCheckedPrice",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastCheckedPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastCheckedAt",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "LastCheckedAt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastNotifiedPrice",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "LastNotifiedPrice",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LastNotifiedAt",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "LastNotifiedAt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"RouteKey"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IdqstP-TwSmyyYo6jQ7dg0-Lxo2bqLF74CTDbAGfJSQ/edit?usp=drivesdk",
"cachedResultName": "Price Log"
},
"authentication": "serviceAccount"
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "262d211e-67a8-4561-9c80-4e8750450c44",
"name": "Encode Flight URL",
"type": "n8n-nodes-base.code",
"position": [
3536,
752
],
"parameters": {
"jsCode": "const results = $input.all();\nconst config = $('Set Search Parameters').first().json;\n\nlet lowestPrice = 999999;\nlet offersFound = results.length;\nlet bestFlightDetails = {};\n\n// 1. Find the lowest price and extract its flight details\nif (offersFound > 0) {\n for (const item of results) {\n const data = item.json;\n \n // Apify returns 'price'\n const priceStr = data.price || \"999999\";\n const price = typeof priceStr === 'number' ? priceStr : parseFloat(priceStr.toString().replace(/[^0-9.]/g, ''));\n \n if (price && price < lowestPrice) {\n lowestPrice = price;\n \n // Save details for the Telegram alert\n bestFlightDetails = {\n airlines: data.airlines || [\"Unknown\"],\n stops: data.stops !== undefined ? data.stops : \"Unknown\",\n durationMinutes: data.totalDuration || \"Unknown\",\n outbound: {\n departureTime: data.outbound?.segments?.[0]?.departureTime || \"Unknown\",\n flightNumber: data.outbound?.segments?.[0]?.flightNumber || \"Unknown\"\n },\n return: {\n departureTime: data.return?.segments?.[0]?.departureTime || \"N/A\",\n flightNumber: data.return?.segments?.[0]?.flightNumber || \"N/A\"\n }\n };\n }\n }\n}\n\n// 2. Build the protobuf Google Flights URL\nfunction buildGoogleFlightsUrl(config) {\n const encodeVarint = (val) => {\n const res = [];\n while (val > 127) {\n res.push((val & 127) | 128);\n val >>>= 7;\n }\n res.push(val);\n return res;\n };\n\n const encodeString = (str) => {\n const bytes = Array.from(Buffer.from(str, 'utf8'));\n return [...encodeVarint(bytes.length), ...bytes];\n };\n\n const cabinMap = { economy: 1, 'premium-economy': 2, business: 3, first: 4 };\n const cabinValue = cabinMap[config.cabinClass] || 1;\n const isRoundTrip = !!config.returnDate;\n\n const payload = [];\n \n // Wrap ALL tags and values in encodeVarint to prevent byte corruption\n payload.push(...encodeVarint(1 << 3 | 0), ...encodeVarint(28)); // query_mode\n payload.push(...encodeVarint(2 << 3 | 0), ...encodeVarint(2)); // query_context\n\n const legs = [{ origin: config.origin, destination: config.destination, date: config.outboundDate }];\n if (isRoundTrip) {\n legs.push({ origin: config.destination, destination: config.origin, date: config.returnDate });\n }\n\n for (const leg of legs) {\n const legBytes = [];\n legBytes.push(...encodeVarint(2 << 3 | 2), ...encodeString(leg.date));\n\n // Google Flights requires strict UPPERCASE IATA codes (e.g., \"JFK\")\n const originCode = leg.origin.toUpperCase();\n const destCode = leg.destination.toUpperCase();\n\n const originBytes = [\n ...encodeVarint(1 << 3 | 0), ...encodeVarint(1), \n ...encodeVarint(2 << 3 | 2), ...encodeString(originCode)\n ];\n legBytes.push(...encodeVarint(13 << 3 | 2), ...encodeVarint(originBytes.length), ...originBytes);\n\n const destBytes = [\n ...encodeVarint(1 << 3 | 0), ...encodeVarint(1), \n ...encodeVarint(2 << 3 | 2), ...encodeString(destCode)\n ];\n legBytes.push(...encodeVarint(14 << 3 | 2), ...encodeVarint(destBytes.length), ...destBytes);\n\n payload.push(...encodeVarint(3 << 3 | 2), ...encodeVarint(legBytes.length), ...legBytes);\n }\n\n for (let i = 0; i < (config.adults || 0); i++) {\n payload.push(...encodeVarint(8 << 3 | 0), ...encodeVarint(1));\n }\n for (let i = 0; i < (config.children || 0); i++) {\n payload.push(...encodeVarint(8 << 3 | 0), ...encodeVarint(2));\n }\n for (let i = 0; i < (config.infants || 0); i++) {\n payload.push(...encodeVarint(8 << 3 | 0), ...encodeVarint(3));\n }\n\n payload.push(...encodeVarint(9 << 3 | 0), ...encodeVarint(cabinValue));\n \n payload.push(...encodeVarint(14 << 3 | 0), ...encodeVarint(1)); // display_flag = 1\n \n const sentinelBytes = [8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01];\n payload.push(...encodeVarint(16 << 3 | 2), ...encodeVarint(sentinelBytes.length), ...sentinelBytes);\n\n payload.push(...encodeVarint(19 << 3 | 0), ...encodeVarint(isRoundTrip ? 1 : 2));\n\n const base64 = Buffer.from(payload)\n .toString('base64')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=+$/, '');\n\n // Added a fallback for currency just in case it's undefined\n return `https://www.google.com/travel/flights/search?tfs=${base64}&curr=${config.currency || 'USD'}&hl=en`;\n}\n\nconst routeKey = `${config.origin}-${config.destination}`;\nconst bookingLink = buildGoogleFlightsUrl(config);\n\n// 3. Output standard schema for downstream nodes\nreturn [\n {\n json: {\n lowestPrice: lowestPrice === 999999 ? null : lowestPrice,\n offersFound: offersFound,\n currency: config.currency,\n routeKey: routeKey,\n bookingLink: bookingLink,\n bestFlightDetails: bestFlightDetails,\n checkedAt: new Date().toISOString()\n }\n }\n];"
},
"typeVersion": 2
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "ce7556e0-55b5-40d9-9dff-53d82d0b5398",
"nodeGroups": [],
"connections": {
"If Alert Needed": {
"main": [
[
{
"node": "Encode Flight URL",
"type": "main",
"index": 0
}
]
]
},
"If Price is Low": {
"main": [
[
{
"node": "Read Price Log from Sheets",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Result to Sheets",
"type": "main",
"index": 0
}
]
]
},
"Every Day at 7am": {
"main": [
[
{
"node": "Set Search Parameters",
"type": "main",
"index": 0
}
]
]
},
"Encode Flight URL": {
"main": [
[
{
"node": "Send Telegram Notification",
"type": "main",
"index": 0
}
]
]
},
"Parse Flight Results": {
"main": [
[
{
"node": "If Price is Low",
"type": "main",
"index": 0
}
]
]
},
"Set Search Parameters": {
"main": [
[
{
"node": "Post to Google Flights API",
"type": "main",
"index": 0
}
]
]
},
"Prepare Alert Decision": {
"main": [
[
{
"node": "If Alert Needed",
"type": "main",
"index": 0
}
]
]
},
"Post to Google Flights API": {
"main": [
[
{
"node": "Parse Flight Results",
"type": "main",
"index": 0
}
]
]
},
"Read Price Log from Sheets": {
"main": [
[
{
"node": "Prepare Alert Decision",
"type": "main",
"index": 0
}
]
]
},
"Send Telegram Notification": {
"main": [
[
{
"node": "Update Price Log in Sheets",
"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.
googleApihttpHeaderAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily to search flight prices via the Apify Google Flights actor, logs each check to Google Sheets, and sends a Telegram alert when the lowest price drops below your threshold and is meaningfully lower than the last notified price. Runs every day at 7am and…
Source: https://n8n.io/workflows/17830/ — 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.
Women creators, homemakers-turned-entrepreneurs, and feminine lifestyle brands who want a graceful, low-lift way to keep an eye on competitor content and spark weekly ideas.
Turn any Amazon India product URL into a fully-edited 10-second lifestyle video and auto-publish it to Instagram, Facebook, X (Twitter), LinkedIn, YouTube, and Threads — with platform-optimized captio
Schedule Trigger runs every 6 hours (customizable) Apify Scraper fetches Upwork jobs matching your criteria Deduplication filters out jobs you've already seen AI Scoring (GPT-4) evaluates fit, client
Automate price monitoring for e-commerce competitors—ideal for retailers, analysts, and pricing teams. Scrapes competitor sites, extracts pricing/stock data via AI, detects changes, and sends instant
> Watch the full Youtube Video Tutorial [](https://youtu.be/Y-wUr2-UYZk)