This workflow corresponds to n8n.io template #9788 — we link there as the canonical source.
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": "IuvYgHrzvtEck50d",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Automate flight Price Comparison",
"tags": [],
"nodes": [
{
"id": "d4a5235a-57c0-42f6-9841-8f45d04dfe1c",
"name": "Webhook - Receive Flight Request",
"type": "n8n-nodes-base.webhook",
"position": [
-1088,
432
],
"parameters": {
"path": "flight-price-compare",
"options": {
"allowedOrigins": "*"
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "81bb3dac-0cd3-4826-8436-573bc393f2c4",
"name": "Parse & Validate Flight Request",
"type": "n8n-nodes-base.code",
"position": [
-864,
432
],
"parameters": {
"jsCode": "// Enhanced Flight Request Parser with NLP\nconst body = $input.first().json.body || {};\nconst query = body.message || body.query || '';\n\n// Extract user details\nconst userEmail = body.email || body.user_email || '';\nconst userName = body.name || body.user_name || 'Traveler';\nconst notifyPriceDrop = body.notify_price_drop || false;\n\n// Greeting handler\nconst greetings = ['hi', 'hello', 'hey', 'start', 'help'];\nif (greetings.some(g => query.toLowerCase().includes(g)) && query.split(' ').length < 5) {\n return [{\n json: {\n status: 'greeting',\n response: `Hi ${userName}! \u2708\ufe0f I'm your Flight Price Comparison Assistant.\\n\\nTell me:\\n\u2705 From city/airport\\n\u2705 To city/airport\\n\u2705 Departure date\\n\u2705 Trip type (one-way/round-trip)\\n\\nExample: \"Flight from New York to London on 25th March, one-way\"`,\n userEmail,\n userName\n }\n }];\n}\n\n// Airport/City codes mapping\nconst airportCodes = {\n 'new york': 'JFK', 'nyc': 'JFK', 'london': 'LHR', 'paris': 'CDG',\n 'dubai': 'DXB', 'singapore': 'SIN', 'tokyo': 'NRT', 'mumbai': 'BOM',\n 'delhi': 'DEL', 'bangalore': 'BLR', 'los angeles': 'LAX', 'chicago': 'ORD',\n 'san francisco': 'SFO', 'boston': 'BOS', 'miami': 'MIA', 'sydney': 'SYD',\n 'melbourne': 'MEL', 'hong kong': 'HKG', 'bangkok': 'BKK', 'amsterdam': 'AMS',\n 'frankfurt': 'FRA', 'toronto': 'YYZ', 'vancouver': 'YVR', 'seattle': 'SEA'\n};\n\nfunction getAirportCode(text) {\n const lower = text.toLowerCase().trim();\n if (/^[A-Z]{3}$/i.test(text)) return text.toUpperCase();\n return airportCodes[lower] || text.toUpperCase();\n}\n\n// Parse dates\nfunction parseDate(text) {\n const monthMap = {\n jan: 0, january: 0, feb: 1, february: 1, mar: 2, march: 2,\n apr: 3, april: 3, may: 4, jun: 5, june: 5,\n jul: 6, july: 6, aug: 7, august: 7, sep: 8, september: 8,\n oct: 9, october: 9, nov: 10, november: 10, dec: 11, december: 11\n };\n\n const dateRegex = /(\\d{1,2})(st|nd|rd|th)?\\s+(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)|\\d{4}-\\d{2}-\\d{2}/gi;\n const matches = [...text.matchAll(dateRegex)];\n const dates = [];\n const currentYear = new Date().getFullYear();\n \n matches.forEach(match => {\n if (match[0].includes('-')) {\n dates.push(new Date(match[0]));\n } else {\n const day = parseInt(match[1]);\n const monthStr = match[3].toLowerCase();\n const month = monthMap[monthStr];\n if (day && month !== undefined) {\n dates.push(new Date(currentYear, month, day));\n }\n }\n });\n \n return dates.sort((a, b) => a - b);\n}\n\n// Extract origin and destination\nlet origin = '';\nlet destination = '';\n\nconst routePattern = /(?:from|leaving)?\\s*([a-z\\s]{3,25})\\s+to\\s+([a-z\\s]{3,25})/i;\nconst routeMatch = query.match(routePattern);\n\nif (routeMatch) {\n origin = routeMatch[1].trim();\n destination = routeMatch[2].trim();\n} else {\n origin = body.from || body.origin || body.departure_airport || '';\n destination = body.to || body.destination || body.arrival_airport || '';\n}\n\nconst originCode = getAirportCode(origin);\nconst destinationCode = getAirportCode(destination);\n\nconst dates = parseDate(query + ' ' + (body.departure_date || ''));\nlet departureDate = dates[0] || null;\nlet returnDate = dates[1] || null;\n\nlet tripType = 'one-way';\nif (query.match(/round[\\s-]?trip|return/i) || returnDate) {\n tripType = 'round-trip';\n}\nif (body.trip_type) {\n tripType = body.trip_type.toLowerCase();\n}\n\nconst passengers = body.passengers || 1;\nconst cabinClass = body.class || body.cabin_class || 'economy';\n\nconst errors = [];\nif (!originCode || originCode.length < 3) errors.push('departure city/airport');\nif (!destinationCode || destinationCode.length < 3) errors.push('arrival city/airport');\nif (!departureDate) errors.push('departure date');\nif (tripType === 'round-trip' && !returnDate) errors.push('return date');\n\nif (errors.length > 0) {\n return [{\n json: {\n status: 'missing_info',\n response: `I need more information: ${errors.join(', ')}.\\n\\nExample: \"Flight from Mumbai to Dubai on 15th March, round-trip returning 20th March\"`,\n userEmail,\n userName\n }\n }];\n}\n\nconst formatDate = (date) => {\n if (!date) return null;\n const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;\n};\n\nconst formatDateISO = (date) => {\n if (!date) return null;\n return date.toISOString().split('T')[0];\n};\n\nreturn [{\n json: {\n status: 'ready',\n origin: originCode,\n destination: destinationCode,\n originCity: origin,\n destinationCity: destination,\n departureDate: formatDate(departureDate),\n returnDate: returnDate ? formatDate(returnDate) : null,\n departureDateISO: formatDateISO(departureDate),\n returnDateISO: formatDateISO(returnDate),\n tripType,\n passengers,\n cabinClass,\n userEmail,\n userName,\n notifyPriceDrop,\n originalQuery: query\n }\n}];"
},
"typeVersion": 2
},
{
"id": "3f5bb362-ca6a-4c1d-a959-9f194fcc7e99",
"name": "Check If Request Valid",
"type": "n8n-nodes-base.if",
"position": [
-640,
432
],
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json.status}}",
"value2": "ready"
}
]
}
},
"typeVersion": 1
},
{
"id": "07df7d09-a38d-4540-b8f8-0e81405e086b",
"name": "Scrape Kayak",
"type": "n8n-nodes-base.ssh",
"onError": "continueErrorOutput",
"position": [
-416,
48
],
"parameters": {
"cwd": "/home/oneclick-server2/",
"command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} kayak",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "2acab75d-4d75-49d7-8c37-dc36740e7636",
"name": "Scrape Skyscanner",
"type": "n8n-nodes-base.ssh",
"onError": "continueErrorOutput",
"position": [
-416,
240
],
"parameters": {
"cwd": "/home/oneclick-server2/",
"command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} skyscanner",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b74690a3-61b9-41df-967b-bf395fb6aade",
"name": "Scrape Expedia",
"type": "n8n-nodes-base.ssh",
"onError": "continueErrorOutput",
"position": [
-416,
432
],
"parameters": {
"cwd": "/home/oneclick-server2/",
"command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} expedia",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "76e69f7a-9683-47ce-bdf0-b673c504a4b2",
"name": "Scrape Google Flights",
"type": "n8n-nodes-base.ssh",
"onError": "continueErrorOutput",
"position": [
-416,
816
],
"parameters": {
"cwd": "/home/oneclick-server2/",
"command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} googleflights",
"authentication": "privateKey"
},
"credentials": {
"sshPrivateKey": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "020178b1-0de8-4cbc-812d-bec95ad92d8f",
"name": "Aggregate & Analyze Prices",
"type": "n8n-nodes-base.code",
"position": [
-64,
336
],
"parameters": {
"jsCode": "// Aggregate flight prices from all platforms\nconst items = $input.all();\nconst searchData = items[0].json;\n\nconst flights = [];\nconst errors = [];\n\nconst platforms = ['Kayak', 'Skyscanner', 'Expedia', 'Google Flights'];\n\nitems.slice(1).forEach((item, index) => {\n const platform = platforms[index];\n const output = item.json.stdout || '';\n \n if (item.json.stderr || !output) {\n errors.push(`${platform}: Unable to fetch`);\n return;\n }\n \n const lines = output.trim().split('\\n');\n \n lines.forEach(line => {\n if (line.includes('|')) {\n const parts = line.split('|');\n if (parts.length >= 6) {\n const price = parseFloat(parts[1].replace(/[^0-9.]/g, ''));\n \n if (price && price > 0) {\n flights.push({\n platform,\n airline: parts[0].trim(),\n price,\n currency: parts[1].match(/[A-Z\u20b9$\u20ac\u00a3]/)?.[0] || '$',\n duration: parts[2].trim(),\n stops: parts[3].trim(),\n departureTime: parts[4].trim(),\n arrivalTime: parts[5].trim(),\n bookingUrl: parts[6]?.trim() || '#',\n cabinClass: searchData.cabinClass\n });\n }\n }\n }\n });\n});\n\nif (flights.length === 0) {\n return [{\n json: {\n status: 'no_results',\n message: 'No flights found for your search criteria.',\n errors,\n ...searchData\n }\n }];\n}\n\nflights.sort((a, b) => a.price - b.price);\n\nconst bestDeal = flights[0];\nconst avgPrice = flights.reduce((sum, f) => sum + f.price, 0) / flights.length;\nconst maxPrice = flights[flights.length - 1].price;\nconst savings = maxPrice - bestDeal.price;\n\nconst directFlights = flights.filter(f => f.stops === '0' || f.stops.toLowerCase().includes('non'));\nconst bestDirectFlight = directFlights.length > 0 ? directFlights[0] : null;\n\nreturn [{\n json: {\n status: 'success',\n ...searchData,\n results: flights,\n bestDeal,\n bestDirectFlight,\n avgPrice: Math.round(avgPrice),\n maxPrice: Math.round(maxPrice),\n savings: Math.round(savings),\n totalResults: flights.length,\n directFlightsCount: directFlights.length,\n errors,\n searchTimestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b195bb98-d48b-442d-8ceb-7809a2f65d15",
"name": "Format Email Report",
"type": "n8n-nodes-base.code",
"position": [
256,
336
],
"parameters": {
"jsCode": "// Format email report\nconst data = $input.first().json;\n\nif (data.status === 'no_results') {\n return [{\n json: {\n subject: `\u274c No Flights - ${data.origin} to ${data.destination}`,\n text: `No flights found for ${data.origin} to ${data.destination} on ${data.departureDate}`,\n ...data\n }\n }];\n}\n\nconst { origin, destination, departureDate, returnDate, tripType, passengers, results, bestDeal, avgPrice, savings } = data;\n\nconst topResults = results.slice(0, 10);\nconst resultsText = topResults.map((f, i) => \n `${i + 1}. ${f.airline} - ${f.currency}${f.price} (${f.stops === '0' ? 'Non-stop' : f.stops + ' stop(s)'}) - ${f.platform}`\n).join('\\n');\n\nconst textReport = `\nFLIGHT PRICE COMPARISON\n${'='.repeat(50)}\n\nRoute: ${origin} \u2192 ${destination}\nDeparture: ${departureDate}\n${returnDate ? `Return: ${returnDate}\\n` : ''}Trip Type: ${tripType}\nPassengers: ${passengers}\n\n\ud83c\udfc6 BEST DEAL\n${'-'.repeat(50)}\n${bestDeal.airline}\nPrice: ${bestDeal.currency}${bestDeal.price}\nDuration: ${bestDeal.duration}\nStops: ${bestDeal.stops === '0' ? 'Non-stop' : bestDeal.stops + ' stop(s)'}\nPlatform: ${bestDeal.platform}\n${savings > 0 ? `\\n\ud83d\udcb0 Save ${bestDeal.currency}${savings} vs highest price!` : ''}\n\n\ud83d\udcca ALL RESULTS (Top 10)\n${'-'.repeat(50)}\n${resultsText}\n\nAverage Price: ${bestDeal.currency}${avgPrice}\nTotal Results: ${results.length}\n\nPrices subject to availability.\nHappy travels! \u2708\ufe0f\n`;\n\nreturn [{\n json: {\n subject: `\u2708\ufe0f ${origin} \u2192 ${destination} - Best: ${bestDeal.currency}${bestDeal.price}`,\n text: textReport,\n ...data\n }\n}];"
},
"typeVersion": 2
},
{
"id": "994f5fa6-2e45-42f7-af12-f98026250c04",
"name": "Send Email Report",
"type": "n8n-nodes-base.emailSend",
"position": [
480,
336
],
"parameters": {
"text": "={{$json.text}}",
"options": {},
"subject": "={{$json.subject}}",
"toEmail": "={{$json.userEmail}}",
"fromEmail": "user@example.com",
"emailFormat": "text"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "8b3fbdb5-879f-4a02-8927-8632f944bd53",
"name": "Webhook Response (Success)",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
784,
336
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ {\n \"success\": true,\n \"message\": \"Flight comparison sent to \" + $json.userEmail,\n \"route\": $json.origin + \" \u2192 \" + $json.destination,\n \"bestPrice\": $json.bestDeal.price,\n \"airline\": $json.bestDeal.airline,\n \"totalResults\": $json.totalResults\n} }}"
},
"typeVersion": 1.1
},
{
"id": "d1d130de-f7f2-42ed-9f2b-591446329e6c",
"name": "Webhook Response (Error)",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-416,
624
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ {\n \"success\": false,\n \"message\": $json.response || \"Request failed\",\n \"status\": $json.status\n} }}"
},
"typeVersion": 1.1
},
{
"id": "35dd7ec1-cb14-4b34-9138-0d8e8af2380a",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1184,
-624
],
"parameters": {
"color": 4,
"width": 484,
"height": 476,
"content": "## \ud83c\udfaf Workflow Purpose\n\n**Smart Flight Price Comparison**\n\nAutomatically compares flight prices across multiple booking platforms and sends detailed reports via email.\n\n### Key Features:\n\u2705 Natural language input\n\u2705 Multi-platform scraping\n\u2705 Best deal identification \n\u2705 Email reports\n\u2705 Real-time responses\n\n### Platforms Compared:\n- Kayak\n- Skyscanner\n- Expedia\n- Google Flights"
},
"typeVersion": 1
},
{
"id": "c312076a-7442-41fe-ac65-692581acde7d",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1408,
96
],
"parameters": {
"color": 5,
"width": 452,
"height": 516,
"content": "## \ud83d\udce5 INPUT STAGE\n\n**Webhook receives:**\n- Flight search query\n- User email\n- Alert preferences\n\n**Example:**\n```json\n{\n \"message\": \"NYC to London March 25\",\n \"email\": \"user@test.com\"\n}\n```"
},
"typeVersion": 1
},
{
"id": "f8aa3a61-314d-4b4e-83bb-477f50a7562a",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-928,
48
],
"parameters": {
"color": 5,
"width": 260,
"height": 612,
"content": "## \ud83e\udde0 PARSE STAGE\n\n**Extracts:**\n- Airport codes\n- Dates (ISO format)\n- Trip type\n- Passengers\n\n**Validates:**\n- Required fields\n- Date formats\n- Airport codes"
},
"typeVersion": 1
},
{
"id": "32ea52fc-41ef-4f5c-bf12-c0b13b9a2866",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-508,
-280
],
"parameters": {
"color": 5,
"width": 280,
"height": 1224,
"content": "## \ud83d\udd0d SCRAPE STAGE\n\n**Parallel scraping:**\n- All platforms simultaneously\n- Continue on failures\n- 30s timeout per scraper\n\n**Output format:**\nAIRLINE|PRICE|DURATION|STOPS|TIME|TIME|URL"
},
"typeVersion": 1
},
{
"id": "d503d1f7-06ca-4d75-96fc-b0a6d3fccb1a",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-112,
-32
],
"parameters": {
"color": 5,
"width": 260,
"height": 660,
"content": "## \ud83d\udcca ANALYZE STAGE\n\n**Processes:**\n- Parse all results\n- Find best deals\n- Calculate stats\n- Sort by price\n\n**Outputs:**\n- Best overall\n- Best direct\n- Avg price\n- Savings"
},
"typeVersion": 1
},
{
"id": "b194c549-bdad-4f31-9ea3-5990caea9b84",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
224,
0
],
"parameters": {
"color": 5,
"width": 388,
"height": 628,
"content": "## \ud83d\udce7 REPORT STAGE\n\n**Email contains:**\n- Flight route & dates\n- Best deal highlight\n- Top 10 results\n- Price statistics\n- Booking links\n\n**Format:**\nPlain text (easy to read)"
},
"typeVersion": 1
},
{
"id": "0e963069-8538-431f-977b-9832b8905af4",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
688,
0
],
"parameters": {
"color": 5,
"width": 260,
"height": 612,
"content": "## \u2705 RESPONSE STAGE\n\n**Success:**\n- Best price found\n- Airline name\n- Total results\n- Email sent confirmation\n\n**Error:**\n- Helpful message\n- What's missing\n- Example format"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "8562102b-7099-407b-ae1f-69da37f8da99",
"connections": {
"Scrape Kayak": {
"main": [
[
{
"node": "Aggregate & Analyze Prices",
"type": "main",
"index": 0
}
]
]
},
"Scrape Expedia": {
"main": [
[
{
"node": "Aggregate & Analyze Prices",
"type": "main",
"index": 0
}
]
]
},
"Scrape Skyscanner": {
"main": [
[
{
"node": "Aggregate & Analyze Prices",
"type": "main",
"index": 0
}
]
]
},
"Send Email Report": {
"main": [
[
{
"node": "Webhook Response (Success)",
"type": "main",
"index": 0
}
]
]
},
"Format Email Report": {
"main": [
[
{
"node": "Send Email Report",
"type": "main",
"index": 0
}
]
]
},
"Scrape Google Flights": {
"main": [
[
{
"node": "Aggregate & Analyze Prices",
"type": "main",
"index": 0
}
]
]
},
"Check If Request Valid": {
"main": [
[
{
"node": "Scrape Kayak",
"type": "main",
"index": 0
},
{
"node": "Scrape Skyscanner",
"type": "main",
"index": 0
},
{
"node": "Scrape Expedia",
"type": "main",
"index": 0
},
{
"node": "Scrape Google Flights",
"type": "main",
"index": 0
}
],
[
{
"node": "Webhook Response (Error)",
"type": "main",
"index": 0
}
]
]
},
"Aggregate & Analyze Prices": {
"main": [
[
{
"node": "Format Email Report",
"type": "main",
"index": 0
}
]
]
},
"Parse & Validate Flight Request": {
"main": [
[
{
"node": "Check If Request Valid",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Receive Flight Request": {
"main": [
[
{
"node": "Parse & Validate Flight Request",
"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.
smtpsshPrivateKey
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates flight price comparison across multiple booking platforms (Kayak, Skyscanner, Expedia, Google Flights). It accepts natural language queries, extracts flight details using NLP, scrapes prices in parallel, identifies the best deals, and sends professional…
Source: https://n8n.io/workflows/9788/ — 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.
Automate the tracking of customer subscription expiry dates, create renewal tasks in ClickUp, and dispatch friendly email reminders before the due date. The workflow listens for incoming subscription
[NooviChat] Onboarding Pós-Pagamento (Exp 6). Uses emailSend. Webhook trigger; 11 nodes.
This n8n workflow template is designed to help system administrators and DevOps professionals monitor key resource usage metrics — CPU, RAM, and Disk — on a VPS (Virtual Private Server). The workflow
This automated n8n workflow processes student applications on a scheduled basis, validates data, updates databases, and sends welcome communications to students and guardians.