AutomationFlowsWeb Scraping › Plan Ai-powered Travel Itineraries with Apify, Openai, and Google Docs

Plan Ai-powered Travel Itineraries with Apify, Openai, and Google Docs

ByTheodoros Mastromanolis @teomastro on n8n.io

Travel agencies, freelance travel planners, or anyone who wants to automate personalized trip planning by combining real-time hotel and flight data with AI-generated recommendations. Collects travel details (airports, dates, travelers) through an n8n form Scrapes the top 5…

Event trigger★★★★☆ complexityAI-powered20 nodesForm Trigger@Apify/N8N Nodes ApifyHTTP RequestChain LlmOpenAI ChatGoogle DocsForm
Web Scraping Trigger: Event Nodes: 20 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #13921 — we link there as the canonical source.

This workflow follows the Chainllm → Form Trigger 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 →

Download .json
{
  "id": "sjh-_Xf1ekuPpodwk_WTV",
  "name": "Plan travel itineraries with AI using Apify, OpenAI, and Google Docs",
  "tags": [],
  "nodes": [
    {
      "id": "c82e7927-b801-4c02-bae7-cbfd2e7e1c4f",
      "name": "Travel Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -896,
        416
      ],
      "parameters": {
        "options": {},
        "formTitle": "\u2708\ufe0f Travel Planner",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Departure Airport (IATA Code)",
              "placeholder": "e.g., JFK, LAX, LHR",
              "requiredField": true
            },
            {
              "fieldLabel": "Destination City",
              "placeholder": "e.g., Paris, Tokyo, Barcelona",
              "requiredField": true
            },
            {
              "fieldLabel": "Destination Airport (IATA Code)",
              "placeholder": "e.g., CDG, NRT, BCN",
              "requiredField": true
            },
            {
              "fieldType": "date",
              "fieldLabel": "Check-in Date",
              "requiredField": true
            },
            {
              "fieldType": "date",
              "fieldLabel": "Check-out Date",
              "requiredField": true
            },
            {
              "fieldType": "number",
              "fieldLabel": "Number of Travelers",
              "placeholder": "1",
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Plan your perfect trip! Enter your travel details below and we'll find the best flights, accommodations, restaurants, and attractions for you."
      },
      "typeVersion": 2.2
    },
    {
      "id": "de1f428d-c665-4016-84f9-cc9de570d228",
      "name": "Set Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        -688,
        416
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "departure_airport",
              "name": "departureAirport",
              "type": "string",
              "value": "={{ $json['Departure Airport (IATA Code)'].toUpperCase() }}"
            },
            {
              "id": "destination_city",
              "name": "destination",
              "type": "string",
              "value": "={{ $json['Destination City'] }}"
            },
            {
              "id": "destination_airport",
              "name": "destinationAirport",
              "type": "string",
              "value": "={{ $json['Destination Airport (IATA Code)'].toUpperCase() }}"
            },
            {
              "id": "checkin_date",
              "name": "checkinDate",
              "type": "string",
              "value": "={{ $json['Check-in Date'] }}"
            },
            {
              "id": "checkout_date",
              "name": "checkoutDate",
              "type": "string",
              "value": "={{ $json['Check-out Date'] }}"
            },
            {
              "id": "travelers",
              "name": "travelers",
              "type": "number",
              "value": "={{ $json['Number of Travelers'] }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0109421d-6ef5-41cf-85d1-b8f892ab59bb",
      "name": "Booking.com Scraper",
      "type": "@apify/n8n-nodes-apify.apify",
      "position": [
        -352,
        240
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "list",
          "value": "oeiQgfg5fsmIJB7Cn",
          "cachedResultUrl": "https://console.apify.com/actors/oeiQgfg5fsmIJB7Cn/input",
          "cachedResultName": "Booking Scraper (voyager/booking-scraper)"
        },
        "customBody": "={\n  \"search\": \"{{ $json.destination }}\",\n  \"checkIn\": \"{{ $json.checkinDate }}\",\n  \"checkOut\": \"{{ $json.checkoutDate }}\",\n  \"rooms\": 1,\n  \"adults\": {{ $json.travelers }},\n  \"children\": 0,\n  \"currency\": \"USD\",\n  \"language\": \"en-us\",\n  \"maxItems\": 5,\n  \"sortBy\": \"bayesian_review_score\"\n}",
        "actorSource": "store"
      },
      "credentials": {
        "apifyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3b5b153e-5078-49c3-a22d-57bc3221e33a",
      "name": "HTTP Request Hotels",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -80,
        240
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.defaultDatasetId }}/items",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth"
      },
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a16337c7-a806-4433-b00a-17603e7abe01",
      "name": "Google Flights Scraper",
      "type": "@apify/n8n-nodes-apify.apify",
      "position": [
        -352,
        480
      ],
      "parameters": {
        "actorId": {
          "__rl": true,
          "mode": "list",
          "value": "1dYHRKkEBHBPd0JM7",
          "cachedResultUrl": "https://console.apify.com/actors/1dYHRKkEBHBPd0JM7/input",
          "cachedResultName": "Google Flights Scraper (johnvc/Google-Flights-Data-Scraper-Flight-and-Price-Search)"
        },
        "customBody": "={\n  \"departure_id\": \"{{ $json.departureAirport }}\",\n  \"arrival_id\": \"{{ $json.destinationAirport }}\",\n  \"outbound_date\": \"{{ $json.checkinDate }}\",\n  \"return_date\": \"{{ $json.checkoutDate }}\",\n  \"adults\": {{ $json.travelers }},\n  \"travel_class\": 1,\n  \"currency\": \"USD\"\n}",
        "actorSource": "store"
      },
      "credentials": {
        "apifyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ab18c15d-6213-41a5-a018-5e21d6cda637",
      "name": "HTTP Request Flights",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -80,
        480
      ],
      "parameters": {
        "url": "={{ $json.output.flightResults }}",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth"
      },
      "credentials": {
        "httpQueryAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "9a30f3b5-0dec-444a-a60c-b8f048ac4cad",
      "name": "AI Recommendations",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -304,
        720
      ],
      "parameters": {
        "text": "=You are a travel expert. Please provide comprehensive travel recommendations for {{ $('Set Variables').item.json.destination }}.\n\nTravel dates: {{ $('Set Variables').item.json.checkinDate }} to {{ $('Set Variables').item.json.checkoutDate }}\nNumber of travelers: {{ $('Set Variables').item.json.travelers }}\n\nPlease provide:\n\n1. **TOP 10 RESTAURANTS** - Include a mix of local cuisine, fine dining, and casual options. For each restaurant provide:\n   - Name\n   - Type of cuisine\n   - Price range ($, $$, $$$, $$$$)\n   - Brief description\n   - Must-try dish\n\n2. **TOP 10 MONUMENTS & HISTORICAL SITES** - Include famous landmarks and hidden gems. For each provide:\n   - Name\n   - Brief history/description\n   - Best time to visit\n   - Estimated visit duration\n   - Entry fee (if any)\n\n3. **TOP 10 PLACES TO VISIT** - Include parks, neighborhoods, markets, viewpoints, etc. For each provide:\n   - Name\n   - Description\n   - Best time to visit\n   - Tips for visitors\n\n4. **LOCAL TIPS** - Provide:\n   - Best transportation options\n   - Local customs to be aware of\n   - Safety tips\n   - Best neighborhoods to stay in\n   - Common scams to avoid\n\n5. **SUGGESTED ITINERARY** - Create a day-by-day suggested itinerary based on the travel dates.\n\nFormat your response in a clear, organized manner with proper sections and bullet points.",
        "promptType": "define"
      },
      "typeVersion": 1.4
    },
    {
      "id": "bca8f877-e48e-4862-877e-924b0ed70ac2",
      "name": "OpenAI Recommendations Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -224,
        896
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "5f433cd4-00c3-454b-a25f-09702f6cc23f",
      "name": "Merge All Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        240,
        496
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3
    },
    {
      "id": "08cd8ec8-c779-49b1-a179-6d0738aa1b6e",
      "name": "Process All Data",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        496
      ],
      "parameters": {
        "jsCode": "// Get all input items from all inputs\nconst allItems = [];\n\n// Input 0: Hotels from Booking.com HTTP Request\nconst hotelsInput = $input.all().filter((item, index) => {\n  return $('HTTP Request Hotels').all().some(h => h.json === item.json);\n});\n\n// Input 1: Flights from Google Flights HTTP Request  \nconst flightsInput = $input.all().filter((item, index) => {\n  return $('HTTP Request Flights').all().some(f => f.json === item.json);\n});\n\n// Input 2: AI Recommendations\nconst aiInput = $input.all().filter((item, index) => {\n  return $('AI Recommendations').all().some(a => a.json === item.json);\n});\n\n// Get trip details from Set Variables\nconst tripDetails = $('Set Variables').first().json;\n\n// Process Hotels - directly from array\nlet hotels = [];\nfor (const item of $('HTTP Request Hotels').all()) {\n  const data = item.json;\n  \n  // If it's an array of hotels directly\n  if (Array.isArray(data)) {\n    hotels = data.slice(0, 5).map(h => ({\n      name: h.name || 'Unknown Hotel',\n      rating: h.rating || 'N/A',\n      price: h.price ? `$${Math.round(h.price)}` : 'N/A',\n      location: h.address?.full || h.location || 'N/A',\n      url: h.url || null,\n      stars: h.stars || null,\n      reviews: h.reviews || null\n    }));\n  }\n  // If it's a single hotel object with these fields\n  else if (data.name && data.url) {\n    hotels.push({\n      name: data.name,\n      rating: data.rating || 'N/A',\n      price: data.price ? `$${Math.round(data.price)}` : 'N/A',\n      location: data.address?.full || 'N/A',\n      url: data.url,\n      stars: data.stars || null,\n      reviews: data.reviews || null\n    });\n  }\n}\n\n// Limit to top 5 hotels, sorted by rating\nif (hotels.length > 5) {\n  hotels = hotels.sort((a, b) => (b.rating || 0) - (a.rating || 0)).slice(0, 5);\n}\n\n// Process Flights\nlet flights = [];\nfor (const item of $('HTTP Request Flights').all()) {\n  const data = item.json;\n  \n  // Handle array response\n  if (Array.isArray(data)) {\n    for (const flightData of data) {\n      if (flightData.best_flights) {\n        flights = flights.concat(flightData.best_flights.map(f => ({\n          airline: f.flights?.[0]?.airline || 'Unknown',\n          flightNumber: f.flights?.[0]?.flight_number || '',\n          price: flightData.price_insights?.lowest_price || f.price || 'N/A',\n          duration: f.total_duration || 'N/A',\n          stops: (f.flights?.length || 1) - 1,\n          departure: f.flights?.[0]?.departure_airport?.time || 'N/A',\n          arrival: f.flights?.[f.flights?.length - 1]?.arrival_airport?.time || 'N/A',\n          departureAirport: f.flights?.[0]?.departure_airport?.id || '',\n          arrivalAirport: f.flights?.[f.flights?.length - 1]?.arrival_airport?.id || '',\n          airplane: f.flights?.[0]?.airplane || '',\n          // No direct booking URL from Google Flights API\n        })));\n      }\n      if (flightData.other_flights) {\n        flights = flights.concat(flightData.other_flights.slice(0, 2).map(f => ({\n          airline: f.flights?.[0]?.airline || 'Unknown',\n          flightNumber: f.flights?.[0]?.flight_number || '',\n          price: f.price || 'N/A',\n          duration: f.total_duration || 'N/A',\n          stops: (f.flights?.length || 1) - 1,\n          departure: f.flights?.[0]?.departure_airport?.time || 'N/A',\n          arrival: f.flights?.[f.flights?.length - 1]?.arrival_airport?.time || 'N/A',\n          departureAirport: f.flights?.[0]?.departure_airport?.id || '',\n          arrivalAirport: f.flights?.[f.flights?.length - 1]?.arrival_airport?.id || '',\n          airplane: f.flights?.[0]?.airplane || '',\n        })));\n      }\n    }\n  }\n  // Handle single object response\n  else if (data.best_flights) {\n    flights = flights.concat(data.best_flights.map(f => ({\n      airline: f.flights?.[0]?.airline || 'Unknown',\n      flightNumber: f.flights?.[0]?.flight_number || '',\n      price: data.price_insights?.lowest_price || f.price || 'N/A',\n      duration: f.total_duration || 'N/A',\n      stops: (f.flights?.length || 1) - 1,\n      departure: f.flights?.[0]?.departure_airport?.time || 'N/A',\n      arrival: f.flights?.[f.flights?.length - 1]?.arrival_airport?.time || 'N/A',\n      departureAirport: f.flights?.[0]?.departure_airport?.id || '',\n      arrivalAirport: f.flights?.[f.flights?.length - 1]?.arrival_airport?.id || '',\n      airplane: f.flights?.[0]?.airplane || '',\n    })));\n  }\n}\n\n// Limit flights to top 5\nflights = flights.slice(0, 5);\n\n// Process AI Recommendations\nlet recommendations = '';\nfor (const item of $('AI Recommendations').all()) {\n  if (item.json.text) {\n    recommendations = item.json.text;\n  } else if (item.json.output) {\n    recommendations = item.json.output;\n  }\n}\n\nreturn [{\n  json: {\n    tripDetails: {\n      destination: tripDetails.destination,\n      departureAirport: tripDetails.departureAirport,\n      destinationAirport: tripDetails.destinationAirport,\n      checkIn: tripDetails.checkinDate,\n      checkOut: tripDetails.checkoutDate,\n      travelers: tripDetails.travelers\n    },\n    hotels: hotels,\n    flights: flights,\n    recommendations: recommendations,\n    generatedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d7efd999-bab6-41c9-844d-dcfc2a5822b3",
      "name": "Create Document",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        816,
        496
      ],
      "parameters": {
        "title": "=Travel Plan - {{ $json.tripDetails.departureAirport }} to {{ $json.tripDetails.destinationAirport }} for {{ $json.tripDetails.travelers }} people",
        "folderId": "YOUR_FOLDER_ID"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "2fc9cb4d-a0bb-4fe3-b964-fdce703d1ce0",
      "name": "Prepare Document Content",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        496
      ],
      "parameters": {
        "jsCode": "const docInfo = $('Create Document').first().json;\nconst travelPlan = $('Process All Data').first().json;\nconst documentId = docInfo.id || docInfo.documentId || docInfo.docId;\n\nlet content = `\u2708\ufe0f TRAVEL PLAN: ${travelPlan.tripDetails.destination}\\n`;\ncontent += `Generated: ${travelPlan.generatedAt}\\n\\n`;\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\ncontent += `\ud83d\udccb TRIP DETAILS\\n`;\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\ncontent += `From: ${travelPlan.tripDetails.departureAirport}\\n`;\ncontent += `To: ${travelPlan.tripDetails.destinationAirport}\\n`;\ncontent += `Check-in: ${travelPlan.tripDetails.checkIn}\\n`;\ncontent += `Check-out: ${travelPlan.tripDetails.checkOut}\\n`;\ncontent += `Travelers: ${travelPlan.tripDetails.travelers}\\n\\n`;\n\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\ncontent += `\u2708\ufe0f FLIGHTS\\n`;\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\nif (travelPlan.flights && travelPlan.flights.length > 0) {\n  travelPlan.flights.forEach((f, i) => {\n    content += `${i+1}. ${f.airline} ${f.flightNumber || ''}\\n`;\n    content += `   \ud83d\udcb0 Price: $${f.price}\\n`;\n    content += `   \u23f1\ufe0f Duration: ${f.duration} min | ${f.stops} stop(s)\\n`;\n    content += `   \ud83d\udeeb ${f.departure} \u2192 \ud83d\udeec ${f.arrival}\\n`;\n    if (f.airplane) content += `   \u2708\ufe0f Aircraft: ${f.airplane}\\n`;\n    content += `\\n`;\n  });\n  content += `\ud83d\udd17 Search flights: https://www.google.com/travel/flights?q=flights%20from%20${travelPlan.tripDetails.departureAirport}%20to%20${travelPlan.tripDetails.destinationAirport}\\n`;\n} else {\n  content += `No flight data available\\n`;\n}\n\ncontent += `\\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\ncontent += `\ud83c\udfe8 TOP RECOMMENDED HOTELS\\n`;\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\nif (travelPlan.hotels && travelPlan.hotels.length > 0) {\n  travelPlan.hotels.forEach((h, i) => {\n    content += `\\n${i+1}. ${h.name}\\n`;\n    content += `   \u2b50 Rating: ${h.rating}/10`;\n    if (h.stars) content += ` (${h.stars}-star)`;\n    if (h.reviews) content += ` - ${h.reviews} reviews`;\n    content += `\\n`;\n    content += `   \ud83d\udcb0 Price: ${h.price}\\n`;\n    content += `   \ud83d\udccd Location: ${h.location}\\n`;\n    if (h.url) content += `   \ud83d\udd17 Book here: ${h.url}\\n`;\n  });\n} else {\n  content += `No hotel data available\\n`;\n}\n\ncontent += `\\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\ncontent += `\ud83c\udfaf AI RECOMMENDATIONS\\n`;\ncontent += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n`;\ncontent += travelPlan.recommendations || 'No recommendations available';\n\nreturn [{\n  json: {\n    documentId: documentId,\n    documentUrl: `https://docs.google.com/document/d/${documentId}/edit`,\n    content: content\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cc3d3c09-77a9-4b46-9404-48a172e3d84a",
      "name": "Update Document",
      "type": "n8n-nodes-base.googleDocs",
      "position": [
        1248,
        496
      ],
      "parameters": {
        "actionsUi": {
          "actionFields": [
            {
              "text": "={{ $json.content }}",
              "action": "insert"
            }
          ]
        },
        "operation": "update",
        "documentURL": "={{ $json.documentId }}"
      },
      "credentials": {
        "googleDocsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "899c6918-0e89-407c-9c41-93113a5cbb74",
      "name": "Prepare Form Ending",
      "type": "n8n-nodes-base.code",
      "position": [
        1472,
        496
      ],
      "parameters": {
        "jsCode": "// Get the document URL from Prepare Document Content node\nconst preparedData = $('Prepare Document Content').first().json;\n\nreturn [{\n  json: {\n    documentUrl: preparedData.documentUrl\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cafd3d14-46ac-4845-9ba6-4ff2656a71f0",
      "name": "Form Ending",
      "type": "n8n-nodes-base.form",
      "position": [
        1696,
        496
      ],
      "parameters": {
        "options": {},
        "operation": "completion",
        "completionTitle": "\u2705 Your travel plan is ready!",
        "completionMessage": "=\ud83d\udcc4 View your document here:\n\n{{ $json.documentUrl }}\n\nYour personalized travel plan includes:\n\u2022 Top 3 recommended hotels\n\u2022 Available flights\n\u2022 Restaurant recommendations\n\u2022 Must-see monuments & attractions\n\u2022 Local tips & suggested itinerary\n\nEnjoy your trip! \u2708\ufe0f\ud83c\udf0d"
      },
      "typeVersion": 2.4
    },
    {
      "id": "84c80c0b-f354-43a1-a0a3-527f7680e916",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        16
      ],
      "parameters": {
        "width": 460,
        "height": 920,
        "content": "## Try It Out!\n### Collect travel details via a form, then scrape flights and hotels while AI generates recommendations \u2014 all compiled into a Google Doc.\n\nGreat for travel agencies, personal trip planning, or any service that needs to deliver a complete travel brief automatically.\n\n### How it works\n* User submits a travel form with airports, dates, and number of travelers.\n* Three parallel tasks run: Booking.com hotel scraping, Google Flights scraping, and OpenAI recommendation generation.\n* Results are merged, formatted, and written to a new Google Doc.\n* The user receives a link to their personalized travel plan.\n\n### Setup steps\n* Create an [Apify](https://apify.com) account and add your API token as both an \"Apify API\" credential and an \"HTTP Query Auth\" credential (parameter name: `token`).\n* Add your OpenAI API key as an \"OpenAI\" credential.\n* Connect Google Docs via OAuth2 and update the `folderId` in the \"Create Document\" node.\n* Activate the workflow and share the form URL.\n\n### Requirements\n* Apify account (for Booking.com and Google Flights scrapers)\n* OpenAI API key\n* Google account with Docs and Drive access\n\n### Need Help?\nJoin the [Discord](https://discord.com/invite/XPKeKXeB7d) or ask in the [Forum](https://community.n8n.io/)!\n\nHappy Hacking!"
      },
      "typeVersion": 1
    },
    {
      "id": "edb7e1c3-61b9-482d-b953-00328f402eec",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 440,
        "height": 420,
        "content": "## 1. Trigger and input\nThe form collects departure/destination airports, dates, and traveler count. The Set node normalizes these into variables used by all downstream nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "389ecca6-1893-4e00-b38f-8314f1b62af2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -432,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 660,
        "content": "## 2. Parallel data fetching\n[Apify integration docs](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-apify/)\n\nThree tasks run in parallel: Booking.com scrapes top 5 hotels by review score, Google Flights scrapes the best flights, and OpenAI generates restaurant, attraction, and itinerary recommendations."
      },
      "typeVersion": 1
    },
    {
      "id": "3c7f0f3a-10de-4e6c-bd88-f89aebfb05a1",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 420,
        "content": "## 3. Merge and process\nAll three data streams are merged into a single item. The Code node structures hotels, flights, and AI recommendations into a clean object for the Google Doc."
      },
      "typeVersion": 1
    },
    {
      "id": "d13a3f3e-fa66-451f-a733-6bde3e4789b6",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        784,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 1120,
        "height": 420,
        "content": "## 4. Generate Google Doc and respond\n[Google Docs node docs](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googledocs/)\n\nA new Google Doc is created, populated with the formatted travel plan, and the form completion page returns the document link to the user."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "e0c24208-6d3e-4cfe-96a2-b008d62805fe",
  "connections": {
    "Travel Form": {
      "main": [
        [
          {
            "node": "Set Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Variables": {
      "main": [
        [
          {
            "node": "Booking.com Scraper",
            "type": "main",
            "index": 0
          },
          {
            "node": "Google Flights Scraper",
            "type": "main",
            "index": 0
          },
          {
            "node": "AI Recommendations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Data": {
      "main": [
        [
          {
            "node": "Process All Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Document": {
      "main": [
        [
          {
            "node": "Prepare Document Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Document": {
      "main": [
        [
          {
            "node": "Prepare Form Ending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process All Data": {
      "main": [
        [
          {
            "node": "Create Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Recommendations": {
      "main": [
        [
          {
            "node": "Merge All Data",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Booking.com Scraper": {
      "main": [
        [
          {
            "node": "HTTP Request Hotels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request Hotels": {
      "main": [
        [
          {
            "node": "Merge All Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Form Ending": {
      "main": [
        [
          {
            "node": "Form Ending",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request Flights": {
      "main": [
        [
          {
            "node": "Merge All Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Google Flights Scraper": {
      "main": [
        [
          {
            "node": "HTTP Request Flights",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Document Content": {
      "main": [
        [
          {
            "node": "Update Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Recommendations Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Recommendations",
            "type": "ai_languageModel",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Travel agencies, freelance travel planners, or anyone who wants to automate personalized trip planning by combining real-time hotel and flight data with AI-generated recommendations. Collects travel details (airports, dates, travelers) through an n8n form Scrapes the top 5…

Source: https://n8n.io/workflows/13921/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

Automate Sales Meeting Prep With Ai & Apify Sent To Whatsapp. Uses gmail, googleCalendar, lmChatOpenAi, informationExtractor. Event-driven trigger; 61 nodes.

Gmail, Google Calendar, OpenAI Chat +5
Web Scraping

This n8n template builds a meeting assistant that compiles timely reminders of upcoming meetings filled with email history and recent LinkedIn activity of other people on the invite. This is then disc

Gmail, Google Calendar, OpenAI Chat +5
Web Scraping

Community nodes are used in this workflow. B2B Sales Teams & SDRs Recruitment Agencies & Tech Recruiters Startup Founders Growth Marketing Teams Scrape Hiring Signals: The workflow starts by using an

Google Sheets, Google Gemini Chat, Output Parser Structured +3
Web Scraping

A customized n8n workflow inspired by the Lead Generation Agent template. It automates B2B lead scraping via Apify, extracts contact emails with AI, sends cold emails via Gmail, and logs every interac

Telegram, Google Sheets, Gmail +4
Web Scraping

This workflow uses the Zyte API to automatically detect and extract structured data from E-commerce sites, Articles, Job Boards, and Search Engine Results (SERP) - no custom CSS selectors required.

Form Trigger, HTTP Request, Form