If you run an OTA, booking platform, or travel tech company, you're managing a relentless stream of booking events, price signals, cancellations, and traveler communications — all on razor-thin margins.
Zapier and Make are cloud-only, meaning traveler PII, payment data, and booking details flow through a third-party server. Self-hosted n8n keeps everything inside your infrastructure — PCI DSS, GDPR, CCPA compliant by design.
Here are 5 production-ready workflows with full import-ready JSON.
1. Booking Confirmation & Itinerary Builder
Trigger: Webhook (new booking created)
Stack: Webhook → Code → Gmail → Google Sheets → Slack
When a booking lands, this workflow:
- Extracts passenger names, routes, dates, booking reference, and fare class
- Builds a formatted HTML itinerary email
- Logs the booking to a tracking sheet
- Pings Slack #bookings with a one-liner summary
{
"name": "Booking Confirmation & Itinerary Builder",
"nodes": [
{
"parameters": { "httpMethod": "POST", "path": "booking-confirmation", "responseMode": "lastNode", "options": {} },
"name": "Webhook", "type": "n8n-nodes-base.webhook", "position": [240, 300]
},
{
"parameters": {
"jsCode": "const booking = $json.body;\nconst passengerName = booking.passenger_name || 'Traveler';\nconst origin = booking.origin;\nconst destination = booking.destination;\nconst departureDate = booking.departure_date;\nconst bookingRef = booking.booking_ref;\nconst fareName = booking.fare_class || 'Standard';\nconst totalPrice = booking.total_price_usd;\n\nconst htmlItinerary = `\n<div style=\"font-family: Arial, sans-serif; max-width: 600px;\">\n <h2 style=\"color: #1a73e8;\">Your Booking is Confirmed!</h2>\n <p>Hi ${passengerName},</p>\n <p>Your booking is confirmed. Here are your travel details:</p>\n <table style=\"width:100%;border-collapse:collapse;\">\n <tr><td style=\"padding:8px;background:#f5f5f5;font-weight:bold;\">Booking Reference</td><td style=\"padding:8px;\">${bookingRef}</td></tr>\n <tr><td style=\"padding:8px;font-weight:bold;\">Route</td><td style=\"padding:8px;\">${origin} → ${destination}</td></tr>\n <tr><td style=\"padding:8px;background:#f5f5f5;font-weight:bold;\">Departure</td><td style=\"padding:8px;\">${departureDate}</td></tr>\n <tr><td style=\"padding:8px;font-weight:bold;\">Total Paid</td><td style=\"padding:8px;\">$${totalPrice}</td></tr>\n </table>\n <p>Safe travels,<br>The Booking Team</p>\n</div>\n`;\n\nreturn [{ json: { ...booking, htmlItinerary, passengerName, origin, destination, departureDate, bookingRef, totalPrice } }];"
},
"name": "Build Itinerary", "type": "n8n-nodes-base.code", "position": [460, 300]
},
{
"parameters": {
"fromEmail": "bookings@yourdomain.com",
"toEmail": "={{ $json.passenger_email }}",
"subject": "Booking Confirmed: {{ $json.bookingRef }} | {{ $json.origin }} → {{ $json.destination }}",
"emailType": "html", "message": "={{ $json.htmlItinerary }}"
},
"name": "Send Confirmation Email", "type": "n8n-nodes-base.gmail", "position": [680, 300]
},
{
"parameters": { "operation": "append", "documentId": "YOUR_SHEET_ID", "sheetName": "Bookings", "columns": { "mappingMode": "autoMapInputData" } },
"name": "Log to Sheets", "type": "n8n-nodes-base.googleSheets", "position": [900, 300]
},
{
"parameters": { "channel": "#bookings", "text": "New booking: {{ $json.passengerName }} | {{ $json.origin }} → {{ $json.destination }} | {{ $json.departureDate }} | Ref: {{ $json.bookingRef }} | ${{ $json.totalPrice }}" },
"name": "Slack Notification", "type": "n8n-nodes-base.slack", "position": [900, 460]
}
],
"connections": {
"Webhook": { "main": [[{ "node": "Build Itinerary", "type": "main", "index": 0 }]] },
"Build Itinerary": { "main": [[{ "node": "Send Confirmation Email", "type": "main", "index": 0 }, { "node": "Log to Sheets", "type": "main", "index": 0 }]] },
"Send Confirmation Email": { "main": [[{ "node": "Slack Notification", "type": "main", "index": 0 }]] }
}
}
2. Price Drop Alert Pipeline
Trigger: Schedule (hourly)
Stack: Schedule → Google Sheets → HTTP Request → Code → IF → Gmail
For platforms that let users set price alerts, this workflow:
- Reads a sheet of watched routes (user email, origin, destination, target price, last price)
- Polls a flight pricing API for current fares
- Calculates the price delta percentage
- Sends a personalized alert email when the price drops ≥3% or hits the target
- Updates the last_price column
{
"name": "Price Drop Alert Pipeline",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 1 }] } },
"name": "Every Hour", "type": "n8n-nodes-base.scheduleTrigger", "position": [240, 300]
},
{
"parameters": { "operation": "readRows", "documentId": "YOUR_SHEET_ID", "sheetName": "PriceAlerts", "options": {} },
"name": "Read Price Alerts", "type": "n8n-nodes-base.googleSheets", "position": [460, 300]
},
{
"parameters": {
"url": "https://partners.api.skyscanner.net/apiservices/v3/flights/indicative/search",
"sendHeaders": true,
"headerParameters": { "parameters": [{ "name": "api-key", "value": "YOUR_API_KEY" }] },
"sendQueryParameters": true,
"queryParameters": { "parameters": [{ "name": "origin", "value": "={{ $json.origin }}" }, { "name": "destination", "value": "={{ $json.destination }}" }] }
},
"name": "Get Current Price", "type": "n8n-nodes-base.httpRequest", "position": [680, 300]
},
{
"parameters": {
"jsCode": "const currentPrice = parseFloat($json.best_price || 0);\nconst alert = $input.first().json;\nconst lastPrice = parseFloat(alert.last_price || currentPrice);\nconst deltaPercent = lastPrice > 0 ? ((lastPrice - currentPrice) / lastPrice) * 100 : 0;\nconst isPriceDrop = deltaPercent >= 3 || currentPrice <= parseFloat(alert.target_price || 0);\nreturn [{ json: { userEmail: alert.user_email, origin: alert.origin, destination: alert.destination, travelDate: alert.travel_date, currentPrice, lastPrice, deltaPercent: Math.round(deltaPercent * 10) / 10, targetPrice: alert.target_price, isPriceDrop, savings: Math.round(lastPrice - currentPrice) } }];"
},
"name": "Calculate Delta", "type": "n8n-nodes-base.code", "position": [900, 300]
},
{
"parameters": { "conditions": { "boolean": [{ "value1": "={{ $json.isPriceDrop }}", "value2": true }] } },
"name": "Price Dropped?", "type": "n8n-nodes-base.if", "position": [1120, 300]
},
{
"parameters": {
"toEmail": "={{ $json.userEmail }}",
"subject": "Price drop: {{ $json.origin }} → {{ $json.destination }} now ${{ $json.currentPrice }}",
"message": "Good news! A fare you're watching just dropped.\n\nRoute: {{ $json.origin }} → {{ $json.destination }}\nNew price: ${{ $json.currentPrice }} (was ${{ $json.lastPrice }})\nYou save: ${{ $json.savings }} ({{ $json.deltaPercent }}% drop)\n\nBook now before it goes back up."
},
"name": "Send Alert Email", "type": "n8n-nodes-base.gmail", "position": [1340, 200]
}
],
"connections": {
"Every Hour": { "main": [[{ "node": "Read Price Alerts", "type": "main", "index": 0 }]] },
"Read Price Alerts": { "main": [[{ "node": "Get Current Price", "type": "main", "index": 0 }]] },
"Get Current Price": { "main": [[{ "node": "Calculate Delta", "type": "main", "index": 0 }]] },
"Calculate Delta": { "main": [[{ "node": "Price Dropped?", "type": "main", "index": 0 }]] },
"Price Dropped?": { "main": [[{ "node": "Send Alert Email", "type": "main", "index": 0 }], []] }
}
}
3. Cancellation Recovery Pipeline
Trigger: Webhook (booking cancelled)
Stack: Webhook → Code → Slack → Wait → Gmail → Google Sheets
When a booking is cancelled, this workflow:
- Immediately alerts your revenue ops team on Slack
- Waits 2 hours (cooling-off period)
- Sends a recovery email with a rebooking credit code
- Logs the recovery attempt
{
"name": "Cancellation Recovery Pipeline",
"nodes": [
{
"parameters": { "httpMethod": "POST", "path": "booking-cancelled", "responseMode": "lastNode", "options": {} },
"name": "Cancellation Webhook", "type": "n8n-nodes-base.webhook", "position": [240, 300]
},
{
"parameters": {
"jsCode": "const b = $json.body;\nconst bookingValue = parseFloat(b.total_price_usd || 0);\nconst creditAmount = Math.round(bookingValue * 0.1);\nreturn [{ json: { passengerName: b.passenger_name || 'Traveler', origin: b.origin, destination: b.destination, bookingValue, bookingRef: b.booking_ref, passengerEmail: b.passenger_email, creditAmount } }];"
},
"name": "Extract Data", "type": "n8n-nodes-base.code", "position": [460, 300]
},
{
"parameters": { "channel": "#revenue-ops", "text": "Cancellation: {{ $json.passengerName }} | {{ $json.origin }} → {{ $json.destination }} | ${{ $json.bookingValue }} | Recovery email in 2h" },
"name": "Alert Revenue Ops", "type": "n8n-nodes-base.slack", "position": [680, 300]
},
{ "parameters": { "amount": 2, "unit": "hours" }, "name": "Wait 2 Hours", "type": "n8n-nodes-base.wait", "position": [900, 300] },
{
"parameters": {
"toEmail": "={{ $json.passengerEmail }}",
"subject": "We've added ${{ $json.creditAmount }} travel credit to your account",
"message": "Hi {{ $json.passengerName }},\n\nWe noticed you cancelled your trip from {{ $json.origin }} to {{ $json.destination }}. Plans change — we get it.\n\nAs a thank-you for booking with us, we've added ${{ $json.creditAmount }} in travel credit to your account, valid for 90 days on any route.\n\nCode: COMEBACK{{ $json.bookingRef }}\n\nWhen you're ready to travel again, we'd love to have you back."
},
"name": "Recovery Email", "type": "n8n-nodes-base.gmail", "position": [1120, 300]
},
{
"parameters": { "operation": "append", "documentId": "YOUR_SHEET_ID", "sheetName": "CancellationRecovery", "columns": { "mappingMode": "autoMapInputData" } },
"name": "Log Recovery", "type": "n8n-nodes-base.googleSheets", "position": [1340, 300]
}
],
"connections": {
"Cancellation Webhook": { "main": [[{ "node": "Extract Data", "type": "main", "index": 0 }]] },
"Extract Data": { "main": [[{ "node": "Alert Revenue Ops", "type": "main", "index": 0 }]] },
"Alert Revenue Ops": { "main": [[{ "node": "Wait 2 Hours", "type": "main", "index": 0 }]] },
"Wait 2 Hours": { "main": [[{ "node": "Recovery Email", "type": "main", "index": 0 }]] },
"Recovery Email": { "main": [[{ "node": "Log Recovery", "type": "main", "index": 0 }]] }
}
}
4. Post-Trip NPS & Review Collection
Trigger: Webhook (trip completed)
Stack: Webhook → Wait 24h → Gmail → Google Sheets (mark sent) → Wait 5d → Check status → IF → Gmail follow-up
Timing drives review volume. This workflow:
- Triggers when a trip is marked complete
- Waits 24 hours for the traveler to arrive home
- Sends a personalized review request
- Waits 5 days
- If no review received, sends one gentle follow-up
{
"name": "Post-Trip NPS & Review Collection",
"nodes": [
{
"parameters": { "httpMethod": "POST", "path": "trip-completed", "responseMode": "lastNode", "options": {} },
"name": "Trip Completed", "type": "n8n-nodes-base.webhook", "position": [240, 300]
},
{ "parameters": { "amount": 1, "unit": "days" }, "name": "Wait 24h", "type": "n8n-nodes-base.wait", "position": [460, 300] },
{
"parameters": {
"toEmail": "={{ $json.body.passenger_email }}",
"subject": "How was your trip to {{ $json.body.destination }}?",
"message": "Hi {{ $json.body.passenger_name }},\n\nHope you had a great trip to {{ $json.body.destination }}!\n\nWe'd love to hear your feedback — takes 2 minutes:\n\nhttps://yourplatform.com/review?ref={{ $json.body.booking_ref }}\n\nThanks for traveling with us!"
},
"name": "Send Review Request", "type": "n8n-nodes-base.gmail", "position": [680, 300]
},
{
"parameters": {
"operation": "update", "documentId": "YOUR_SHEET_ID", "sheetName": "Bookings",
"filtersUI": { "values": [{ "lookupColumn": "booking_ref", "lookupValue": "={{ $json.body.booking_ref }}" }] },
"fieldsUi": { "values": [{ "column": "review_requested", "fieldValue": "TRUE" }, { "column": "review_requested_at", "fieldValue": "={{ $now.toISO() }}" }] }
},
"name": "Mark Requested", "type": "n8n-nodes-base.googleSheets", "position": [900, 300]
},
{ "parameters": { "amount": 5, "unit": "days" }, "name": "Wait 5 Days", "type": "n8n-nodes-base.wait", "position": [1120, 300] },
{
"parameters": {
"operation": "readRows", "documentId": "YOUR_SHEET_ID", "sheetName": "Bookings",
"options": { "filters": { "values": [{ "column": "booking_ref", "value": "={{ $json.body.booking_ref }}" }] } }
},
"name": "Check Review Status", "type": "n8n-nodes-base.googleSheets", "position": [1340, 300]
},
{
"parameters": { "conditions": { "string": [{ "value1": "={{ $json.review_received }}", "operation": "notEqual", "value2": "TRUE" }] } },
"name": "No Review?", "type": "n8n-nodes-base.if", "position": [1560, 300]
},
{
"parameters": {
"toEmail": "={{ $json.passenger_email }}",
"subject": "Still time to share your {{ $json.destination }} experience",
"message": "Hi {{ $json.passenger_name }},\n\nJust a quick reminder — we'd still love to hear about your trip to {{ $json.destination }}.\n\nhttps://yourplatform.com/review?ref={{ $json.booking_ref }}\n\nOnly takes 60 seconds. Thanks!"
},
"name": "Follow-up", "type": "n8n-nodes-base.gmail", "position": [1780, 200]
}
],
"connections": {
"Trip Completed": { "main": [[{ "node": "Wait 24h", "type": "main", "index": 0 }]] },
"Wait 24h": { "main": [[{ "node": "Send Review Request", "type": "main", "index": 0 }]] },
"Send Review Request": { "main": [[{ "node": "Mark Requested", "type": "main", "index": 0 }]] },
"Mark Requested": { "main": [[{ "node": "Wait 5 Days", "type": "main", "index": 0 }]] },
"Wait 5 Days": { "main": [[{ "node": "Check Review Status", "type": "main", "index": 0 }]] },
"Check Review Status": { "main": [[{ "node": "No Review?", "type": "main", "index": 0 }]] },
"No Review?": { "main": [[{ "node": "Follow-up", "type": "main", "index": 0 }], []] }
}
}
5. Daily Revenue & Booking Ops Report
Trigger: Schedule (daily 7 AM)
Stack: Schedule → Google Sheets → Code → Gmail + Slack
Every morning, this workflow delivers a formatted HTML report with:
- Total bookings and revenue
- Average order value (AOV)
- Cancellation rate
- Top 3 routes by revenue
- Week-over-week change
{
"name": "Daily Travel Ops Report",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "cronExpression", "expression": "0 7 * * *" }] } },
"name": "7 AM Daily", "type": "n8n-nodes-base.scheduleTrigger", "position": [240, 300]
},
{
"parameters": { "operation": "readRows", "documentId": "YOUR_SHEET_ID", "sheetName": "Bookings", "options": {} },
"name": "Read Bookings", "type": "n8n-nodes-base.googleSheets", "position": [460, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nconst yesterday = new Date(today);\nyesterday.setDate(today.getDate() - 1);\nconst yStr = yesterday.toISOString().split('T')[0];\nconst wAgoStr = new Date(today.getTime() - 7*86400000).toISOString().split('T')[0];\nconst all = $input.all().map(i => i.json);\nconst rows = all.filter(r => r.booking_date === yStr);\nconst lastWeekRows = all.filter(r => r.booking_date === wAgoStr);\nconst active = rows.filter(r => r.status !== 'cancelled');\nconst bookings = active.length;\nconst cancels = rows.filter(r => r.status === 'cancelled').length;\nconst revenue = active.reduce((s, r) => s + parseFloat(r.total_price_usd || 0), 0);\nconst aov = bookings > 0 ? revenue / bookings : 0;\nconst cancelRate = rows.length > 0 ? cancels / rows.length * 100 : 0;\nconst lwRev = lastWeekRows.filter(r => r.status !== 'cancelled').reduce((s, r) => s + parseFloat(r.total_price_usd || 0), 0);\nconst wowPct = lwRev > 0 ? (revenue - lwRev) / lwRev * 100 : 0;\nconst routeMap = {};\nactive.forEach(r => {\n const k = `${r.origin} → ${r.destination}`;\n if (!routeMap[k]) routeMap[k] = { count: 0, rev: 0 };\n routeMap[k].count++;\n routeMap[k].rev += parseFloat(r.total_price_usd || 0);\n});\nconst topRoutes = Object.entries(routeMap).sort((a, b) => b[1].rev - a[1].rev).slice(0, 3);\nconst routeRows = topRoutes.map(([route, d]) => `<tr><td style='padding:6px;'>${route}</td><td style='padding:6px;text-align:center;'>${d.count}</td><td style='padding:6px;text-align:right;'>$${Math.round(d.rev)}</td></tr>`).join('');\nconst wowColor = wowPct >= 0 ? 'green' : 'red';\nconst html = `<div style='font-family:Arial;max-width:680px;'><h2 style='color:#1a73e8;'>Travel Ops Report — ${yStr}</h2><table style='width:100%;border-collapse:collapse;margin-bottom:20px;'><tr style='background:#f5f5f5;'><td style='padding:8px;font-weight:bold;'>Bookings</td><td style='padding:8px;'>${bookings}</td></tr><tr><td style='padding:8px;font-weight:bold;'>Revenue</td><td style='padding:8px;'>$${Math.round(revenue)} <span style='color:${wowColor};font-size:12px;'>(${wowPct >= 0 ? '+' : ''}${Math.round(wowPct)}% WoW)</span></td></tr><tr style='background:#f5f5f5;'><td style='padding:8px;font-weight:bold;'>AOV</td><td style='padding:8px;'>$${Math.round(aov)}</td></tr><tr><td style='padding:8px;font-weight:bold;'>Cancellations</td><td style='padding:8px;'>${cancels} (${Math.round(cancelRate)}%)</td></tr></table><h3>Top Routes by Revenue</h3><table style='width:100%;border-collapse:collapse;'><tr style='background:#e8f0fe;'><th style='padding:6px;text-align:left;'>Route</th><th style='padding:6px;'>Bookings</th><th style='padding:6px;'>Revenue</th></tr>${routeRows || '<tr><td colspan=3 style=padding:6px;>No data</td></tr>'}</table></div>`;\nreturn [{ json: { html, bookings, revenue: Math.round(revenue), aov: Math.round(aov), cancelRate: Math.round(cancelRate), wowPct: Math.round(wowPct), yStr } }];"
},
"name": "Build Report", "type": "n8n-nodes-base.code", "position": [680, 300]
},
{
"parameters": {
"toEmail": "ops@yourdomain.com",
"subject": "Travel Ops Report — {{ $json.yStr }} | ${{ $json.revenue }} | {{ $json.bookings }} bookings",
"emailType": "html", "message": "={{ $json.html }}"
},
"name": "Email Report", "type": "n8n-nodes-base.gmail", "position": [900, 260]
},
{
"parameters": { "channel": "#ops-reporting", "text": "Daily ({{ $json.yStr }}): {{ $json.bookings }} bookings | ${{ $json.revenue }} revenue | AOV ${{ $json.aov }} | Cancel {{ $json.cancelRate }}%" },
"name": "Slack Summary", "type": "n8n-nodes-base.slack", "position": [900, 420]
}
],
"connections": {
"7 AM Daily": { "main": [[{ "node": "Read Bookings", "type": "main", "index": 0 }]] },
"Read Bookings": { "main": [[{ "node": "Build Report", "type": "main", "index": 0 }]] },
"Build Report": { "main": [[{ "node": "Email Report", "type": "main", "index": 0 }, { "node": "Slack Summary", "type": "main", "index": 0 }]] }
}
}
Why self-hosted n8n for Travel Tech?
| Requirement | n8n (self-hosted) | Zapier | Make.com |
|---|---|---|---|
| Traveler PII stays in your infra | ✅ | ❌ | ❌ |
| PCI DSS / payment data control | ✅ | ❌ | ❌ |
| GDPR / CCPA by design | ✅ | Partial | Partial |
| Webhook processing volume | Unlimited | Capped | Capped |
| Custom pricing logic (JS/Python) | ✅ | Limited | Limited |
| Cost at scale | ~$0 (self-hosted) | $$$$ | $$$ |
For OTAs and booking platforms, the data sovereignty argument is compelling: traveler names, passport numbers, payment details, and booking records cannot flow through a third-party automation cloud without significant compliance risk.
Ready-to-use workflow templates
These 5 workflows — plus 10 more covering demand forecasting, partner API integrations, affiliate payouts, and customer win-back pipelines — are available as tested, import-ready JSON templates at FlowKit — n8n Automation Templates.
Drop the ZIP into n8n (File → Import → From File), swap in your API keys, and have it running in under 10 minutes.
Running a travel tech automation that didn't make this list? Drop it in the comments — I add new workflows every week.
Top comments (0)