AutomationFlowsGeneral › Flugsuche Explore (bulgaria Christmas 2026)

Flugsuche Explore (bulgaria Christmas 2026)

Flugsuche Explore (Bulgaria Christmas 2026). Event-driven trigger; 8 nodes.

Event trigger★★★★☆ complexity8 nodes
General Trigger: Event Nodes: 8 Complexity: ★★★★☆ Added:

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
{
  "name": "Flugsuche Explore (Bulgaria Christmas 2026)",
  "nodes": [
    {
      "parameters": {},
      "id": "cbfbf6b6-785c-4fd3-abaf-7f03ae0adba7",
      "name": "Manuell starten",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -320,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "cfg-origins",
              "name": "origins",
              "value": "HHN, DTM, EIN, CRL, CGN, FRA",
              "type": "string"
            },
            {
              "id": "cfg-from",
              "name": "dateFrom",
              "value": "2026-12-23",
              "type": "string"
            },
            {
              "id": "cfg-to",
              "name": "dateTo",
              "value": "2027-01-06",
              "type": "string"
            },
            {
              "id": "cfg-mindays",
              "name": "minDays",
              "value": 12,
              "type": "number"
            },
            {
              "id": "cfg-pax",
              "name": "erwachsene",
              "value": 5,
              "type": "number"
            },
            {
              "id": "cfg-direkt",
              "name": "nurDirekt",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "cfg-ziel",
              "name": "ziel",
              "value": "",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "7f26e1b9-92e7-42f7-b521-9932a1fc3c64",
      "name": "Konfig",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -128,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Job-Generator f\u00fcr Google-Flights \"Explore\" (anywhere). Wird wortgleich in den\n// n8n-Code-Node \"Jobs generieren\" kopiert. Keine externen Abh\u00e4ngigkeiten.\nconst BASE_TFS = 'CBwQAxojEgoyMDI2LTEwLTIwagcIARIDRUlOcgwIBBIIL20vMDJqNzEaIxIKMjAyNi0xMC0zMGoMCAQSCC9tLzAyajcxcgcIARIDRUlOQAFIAXABggEECAkQA5gBAbIBBBgBIAE';\n// Nonstop-Variante (live aus der Explore-UI \u201eNur Nonstop-Fl\u00fcge\" eingefangen): pro\n// Slice ist direkt nach dem Datum ein `28 00` (Feld 5 = 0 = nur Direktflug) eingef\u00fcgt.\n// Eigene Basis-Vorlage statt Byte-Insert, weil das Einf\u00fcgen sonst die Slice-L\u00e4ngen-\n// Pr\u00e4fixe (1a 23 -> 1a 25) brechen w\u00fcrde; Origin/Datum/Pax sind hier gleich substituierbar.\nconst BASE_TFS_NONSTOP = 'CBwQAxolEgoyMDI2LTEwLTIwKABqBwgBEgNFSU5yDAgEEggvbS8wMmo3MRolEgoyMDI2LTEwLTMwKABqDAgEEggvbS8wMmo3MXIHCAESA0VJTkABSAFwAYIBBAgJEAOYAQGyAQQYASAB';\nconst TFU = 'GioaKAoSCZT42FsUmUVAEZaEQsDfFjlAEhIJRhogG-qzQUARLAmFgD-SIUA';\n\nconst b64urlDecode = s => Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('latin1');\nconst b64urlEncode = s => Buffer.from(s, 'latin1').toString('base64').replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n\n// EIN->origin (beide Slices), Daten ersetzen, Pax = N x (40 01) direkt vor 48 01.\n// nurDirekt -> Nonstop-Basis-Vorlage (sonst \u201ebeliebige Stopps\").\nfunction buildTfs(origin, out, ret, erwachsene, nurDirekt = false) {\n  let s = b64urlDecode(nurDirekt ? BASE_TFS_NONSTOP : BASE_TFS);\n  s = s.split('EIN').join(origin);\n  s = s.split('2026-10-20').join(out);\n  s = s.split('2026-10-30').join(ret);\n  const pax = '\\x40\\x01'.repeat(Math.max(1, erwachsene));\n  s = s.replace(/(?:\\x40\\x01)+\\x48\\x01/, pax + '\\x48\\x01');\n  return b64urlEncode(s);\n}\n\nfunction buildUrl(origin, out, ret, erwachsene, nurDirekt = false) {\n  const tfs = buildTfs(origin, out, ret, erwachsene, nurDirekt);\n  return `https://www.google.com/travel/explore?tfs=${tfs}&tfu=${TFU}&hl=de&curr=EUR`;\n}\n\n// Abflug ab dateFrom; R\u00fcckflug von out+minDays bis dateTo (alle l\u00e4ngeren Aufenthalte).\nfunction datePairs(dateFrom, dateTo, minDays) {\n  const DAY = 86400000;\n  const from = new Date(dateFrom + 'T00:00:00Z');\n  const to = new Date(dateTo + 'T00:00:00Z');\n  const iso = d => d.toISOString().slice(0, 10);\n  const pairs = [];\n  for (let o = new Date(from); o <= to; o = new Date(+o + DAY)) {\n    for (let r = new Date(+o + minDays * DAY); r <= to; r = new Date(+r + DAY)) {\n      pairs.push({ out: iso(o), ret: iso(r) });\n    }\n  }\n  return pairs;\n}\n\n// Destinations-Deep-Link: h\u00e4ngt die Ziel-MID als Ortsfilter an die Job-tfs (Feld 22,\n// Sub-Feld 5) \u2014 exakt das, was ein Klick auf eine Explore-Kachel erzeugt (live\n// verifiziert). Die MID stammt aus dem `data-code` der <li>-Ziel-Kachel. WORTGLEICH\n// (funktional) in server.js kopiert. Origin/Datum/Nonstop der Job-tfs bleiben erhalten.\nfunction exploreDeepLink(tfs, mid) {\n  if (!mid) return null;\n  const place = '\\x2a' + String.fromCharCode(4 + mid.length) + '\\x08\\x02\\x12' + String.fromCharCode(mid.length) + mid;\n  const content = '\\x18\\x01\\x20\\x01' + place;                    // f22-Inhalt: Default + Ort\n  const newF22 = '\\xb2\\x01' + String.fromCharCode(content.length) + content;\n  const s = b64urlDecode(tfs).replace('\\xb2\\x01\\x04\\x18\\x01\\x20\\x01', newF22);\n  return `https://www.google.com/travel/explore?tfs=${b64urlEncode(s)}&tfu=${TFU}&hl=de&curr=EUR`;\n}\n\nfunction generateJobs(cfg) {\n  const origins = String(cfg.origins).split(',').map(s => s.trim().toUpperCase()).filter(Boolean);\n  const erwachsene = Number(cfg.erwachsene) || 1;\n  // Default: nur Direktfl\u00fcge. false nur bei explizitem false/\"false\".\n  const nurDirekt = !(cfg.nurDirekt === false || cfg.nurDirekt === 'false');\n  const pairs = datePairs(cfg.dateFrom, cfg.dateTo, Number(cfg.minDays));\n  const jobs = [];\n  for (const origin of origins) {\n    for (const { out, ret } of pairs) {\n      jobs.push({ id: `${origin}|${out}|${ret}`, url: buildUrl(origin, out, ret, erwachsene, nurDirekt) });\n    }\n  }\n  return jobs;\n}\n\n// \u2500\u2500 n8n-Treiber \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// State-Datei zu Lauf-Beginn zur\u00fccksetzen: sonst h\u00e4ngt ein neuer Lauf seine Jobs an\n// die Altlast eines zuvor ABGEBROCHENEN Laufs an (Aggregieren l\u00f6scht sie nur am Ende).\n// So spiegelt der Output immer NUR den aktuellen Lauf -> Dateien je Run frisch.\ntry { require('fs').unlinkSync('/tmp/flights_explore_state.json'); } catch (e) {}\nconst cfg = $('Konfig').first().json;\nreturn generateJobs(cfg).map(j => ({ json: j }));\n"
      },
      "id": "5203d989-fe6e-43ac-b422-feb750136c5c",
      "name": "Jobs generieren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        64,
        0
      ]
    },
    {
      "parameters": {
        "batchSize": 5,
        "options": {}
      },
      "id": "17fd5fa4-c93b-4e15-a328-d63b15c9a13f",
      "name": "Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        256,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Ein Batch (<= batchSize Jobs) an /flights-explore schicken und die per-Job-\n// Ergebnisse an eine State-Datei anh\u00e4ngen. Der HTTP-Aufruf l\u00e4uft per http.request\n// INNERHALB des Code-Nodes (wie 'Daten abrufen' der Dividenden-Analyse) \u2014 der\n// n8n-HTTP-Request-Node h\u00e4ngt sonst ein rohes IncomingMessage-Objekt ans Item,\n// das beim Serialisieren in den JS-Task-Runner eine zirkul\u00e4re Struktur wirft.\n// Akkumulation \u00fcber die Datei, weil n8n-Loop-Scoping sonst nur den letzten Batch s\u00e4he.\nconst http = require('http');\nconst fs = require('fs');\nconst STATE = '/tmp/flights_explore_state.json';\nconst jobs = $input.all().map(i => i.json);\nconst body = JSON.stringify({ jobs, politenessMs: 1200, maxRetries: 5 });\n// HTTP-Timeout 1500s: bei Google-Drosselung kann ein 5er-Batch (5 Retries/Job) mehrere\n// Minuten brauchen; muss unter N8N_RUNNERS_TASK_TIMEOUT (1800s) bleiben, aber deutlich\n// \u00fcber dem Worst Case (~8min), damit der Endpoint mit failed-Jobs antwortet statt der\n// http.request vorher abzubrechen (sonst crasht der ganze Lauf).\n// Antwort robust einlesen: Status pr\u00fcfen, nur JSON parsen. Eine nicht-JSON-Antwort\n// (HTML-Fehlerseite, leeres Paket, 5xx unter Last) darf NICHT den ganzen Lauf killen.\nlet results;\ntry {\n  const resp = await new Promise((resolve, reject) => {\n    const req = http.request(\n      { hostname: 'playwright', port: 3001, path: '/flights-explore', method: 'POST',\n        headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 1500000 },\n      (res) => { let d = ''; res.on('data', c => d += c); res.on('end', () => {\n        if (res.statusCode < 200 || res.statusCode >= 300) return reject(new Error('HTTP ' + res.statusCode + ': ' + d.slice(0, 120)));\n        try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('Nicht-JSON-Antwort: ' + d.slice(0, 80).replace(/\\s+/g, ' '))); }\n      }); }\n    );\n    req.on('error', reject);\n    req.on('timeout', () => req.destroy(new Error('flights-explore timeout')));\n    req.write(body); req.end();\n  });\n  results = (resp && resp.results) || [];\n} catch (e) {\n  // Batch-Fehler abfangen: alle Jobs dieses Batches als failed markieren, Lauf weiterlaufen.\n  results = jobs.map(j => ({ id: j.id, dests: {}, count: 0, error: 'batch: ' + e.message }));\n}\nif (results.length) fs.appendFileSync(STATE, results.map(r => JSON.stringify(r)).join('\\n') + '\\n');\n// 10s Politeness-Pause zwischen den Batches (gegen Rate-Limiting der Datacenter-IP).\nawait new Promise(r => setTimeout(r, 10000));\nreturn [{ json: { batch: 'ok', anzahl: results.length } }];\n"
      },
      "id": "f80233a6-104e-408c-976a-ad03ce76b256",
      "name": "Scrape Batch",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        512,
        128
      ]
    },
    {
      "parameters": {
        "jsCode": "// Aggregation f\u00fcr Google-Flights \"Explore\". Reine Funktion, WORTGLEICH in den\n// n8n-Code-Node \"Aggregieren\" kopiert (symmetrisch zu jobgen.js / parse-explore.js;\n// das n8n-Geklebe \u2014 $input.all() einsammeln, params via $('Jobs generieren') \u2014 bleibt\n// im Node, die testbare Logik hier). Keine externen Abh\u00e4ngigkeiten.\n//\n// Eingabe : jobResults = [{ id:\"IATA|out|ret\", dests:{stadt:{preis,dauer,stopps,url}}, count, error }]\n//           params     = { origins, dateFrom, dateTo, minDays, erwachsene }\n// Ausgabe : [{ filename:\"flights_explore_results.txt\", content }, { filename:\"...json\", content }]\n\n// \"2026-10-18\" -> \"18.10.2026\"\nfunction deDate(iso) {\n  const m = String(iso).match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n  return m ? `${m[3]}.${m[2]}.${m[1]}` : String(iso);\n}\n\nfunction aggregate(jobResults, params) {\n  params = params || {};\n  // stadt -> { \"out|ret\" -> { preis, airport, out, ret, dauer, stopps, url } }\n  // Pro (Ziel \u00d7 Datumspaar) das Minimum \u00dcBER die Origins; Datumspaare bleiben erhalten.\n  const byCity = {};\n  const raw = {};    // id -> { iata, out, ret, dests, count, error }\n  let failed = 0;\n\n  for (const job of jobResults || []) {\n    const [airport, out, ret] = String(job.id).split('|');\n    const dests = job.dests || {};\n    raw[job.id] = { iata: airport, out, ret, dests, count: job.count || 0, error: job.error || null };\n    if (job.error || Object.keys(dests).length === 0) failed++;\n\n    for (const [stadt, d] of Object.entries(dests)) {\n      if (d == null || typeof d.preis !== 'number') continue;\n      const key = out + '|' + ret;\n      const slot = (byCity[stadt] = byCity[stadt] || {});\n      if (!slot[key] || d.preis < slot[key].preis) {\n        slot[key] = { preis: d.preis, airport, out, ret, dauer: d.dauer || null, stopps: d.stopps || null, url: d.url || null };\n      }\n    }\n  }\n\n  // Alle (Ziel \u00d7 Datumspaar)-Zeilen flach, global aufsteigend nach Preis sortiert.\n  // Optionaler Ziel-Filter (params.ziel): nur St\u00e4dte, deren Name den Filter enth\u00e4lt\n  // (case-insensitive, Teilstring); leer/nicht gesetzt = alle Ziele wie bisher.\n  const zielFilter = String(params.ziel || '').trim().toLowerCase();\n  const rows = [];\n  for (const [stadt, slots] of Object.entries(byCity)) {\n    if (zielFilter && !stadt.toLowerCase().includes(zielFilter)) continue;\n    for (const b of Object.values(slots)) rows.push(Object.assign({ stadt }, b));\n  }\n  rows.sort((a, b) => a.preis - b.preis || a.stadt.localeCompare(b.stadt) || a.out.localeCompare(b.out));\n\n  const total = (jobResults || []).length;\n  const zielAnzahl = new Set(rows.map(r => r.stadt)).size;\n  const direktNote = (params.nurDirekt === false || params.nurDirekt === 'false') ? '' : ' | nur Direktfl\u00fcge';\n  const zielNote = zielFilter ? ` | Ziel-Filter: ${params.ziel}` : '';\n\n  // \u2500\u2500 TXT: pro Ziel \u00d7 Datumspaar eine Zeile, global aufsteigend nach Preis \u2500\u2500\u2500\u2500\u2500\n  const head = [\n    'Google-Flights Explore \u2014 g\u00fcnstigste Ziele (Preis pro Person, aufsteigend)',\n    `Abflugh\u00e4fen: ${params.origins || '?'} | Zeitfenster: ${deDate(params.dateFrom)} \u2013 ${deDate(params.dateTo)} | min. ${params.minDays} Tage | ${params.erwachsene || 1} Erwachsene(r)${direktNote}${zielNote}`,\n    `Ziele: ${zielAnzahl} | Datumspaar-Treffer: ${rows.length} | Jobs: ${total} (${failed} fehlgeschlagen / ohne Preis)`,\n    ''\n  ];\n  const body = rows.map(b => {\n    const zeile = `${b.preis} \u20ac | ${b.stadt} | ab ${b.airport} | ${deDate(b.out)} \u2013 ${deDate(b.ret)} | ${b.dauer || '?'} | ${b.stopps || '?'}`;\n    return b.url ? `${zeile}\\n       ${b.url}` : zeile;\n  });\n  const txt = head.concat(body).join('\\n') + '\\n';\n\n  return [\n    { filename: 'flights_explore_results_christmas.txt', content: txt },\n    { filename: 'flights_explore_results_christmas.json', content: JSON.stringify(raw, null, 2) }\n  ];\n}\n\n// \u2500\u2500 n8n-Treiber \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst fs = require('fs');\nconst STATE = '/tmp/flights_explore_state.json';\nlet jobResults = [];\nif (fs.existsSync(STATE)) {\n  jobResults = fs.readFileSync(STATE, 'utf8').trim().split('\\n').filter(Boolean).map(l => JSON.parse(l));\n  fs.unlinkSync(STATE); // idempotent: ein zweiter Aufruf f\u00e4nde nichts mehr\n}\nconst params = $('Konfig').first().json;\nreturn aggregate(jobResults, params).map(f => ({ json: f }));\n"
      },
      "id": "9a0151ae-7bb2-4eb8-853e-3a3d8b3e2092",
      "name": "Aggregieren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        512,
        -128
      ]
    },
    {
      "parameters": {
        "jsCode": "// Die beiden fertigen Dateien (txt + json) je per http.request an /write-output\n// schicken (server.js schreibt sie nach /home/pwuser/output). http.request statt\n// HTTP-Request-Node, damit kein rohes IncomingMessage-Objekt in die Execution-Daten\n// gelangt (zirkul\u00e4re Struktur -> DB-Save bricht sonst).\nconst http = require('http');\nconst out = [];\nfor (const it of $input.all()) {\n  const body = JSON.stringify({ filename: it.json.filename, content: it.json.content });\n  const r = await new Promise((resolve, reject) => {\n    const req = http.request(\n      { hostname: 'playwright', port: 3001, path: '/write-output', method: 'POST',\n        headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 30000 },\n      (res) => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); }\n    );\n    req.on('error', reject);\n    req.write(body); req.end();\n  });\n  out.push({ json: r });\n}\nreturn out;\n"
      },
      "id": "b48446dd-3485-496b-a217-33981085cac4",
      "name": "Dateien schreiben",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        768,
        -128
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 3
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -320,
        -160
      ],
      "id": "5cbe261c-7f2b-4980-b8a3-4ec10e26dd1e",
      "name": "Schedule Trigger"
    }
  ],
  "connections": {
    "Manuell starten": {
      "main": [
        [
          {
            "node": "Konfig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Konfig": {
      "main": [
        [
          {
            "node": "Jobs generieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jobs generieren": {
      "main": [
        [
          {
            "node": "Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batches": {
      "main": [
        [
          {
            "node": "Aggregieren",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Batch": {
      "main": [
        [
          {
            "node": "Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregieren": {
      "main": [
        [
          {
            "node": "Dateien schreiben",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Konfig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "174c882f-1ff2-4d30-82a4-4f396af8dc8a",
  "id": "WqsfNHMnSvmXZEXV",
  "tags": []
}
Pro

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

About this workflow

Flugsuche Explore (Bulgaria Christmas 2026). Event-driven trigger; 8 nodes.

Source: https://github.com/cupakob/ai-services/blob/b1a4585537909b3bfbc7944454091d3053f9c128/n8n/workflows/flights/flight-search-christmas.json — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

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

General

Reagendamiento. Uses executeWorkflowTrigger, redis, n8n-nodes-evolution-api, dataTable. Event-driven trigger; 73 nodes.

Execute Workflow Trigger, Redis, N8N Nodes Evolution Api +2
General

Blotato. Uses googleSheets, @blotato/n8n-nodes-blotato. Event-driven trigger; 65 nodes.

Google Sheets, @Blotato/N8N Nodes Blotato
General

This template is a hands-on, practical exam designed to help you master n8n Expressions—the key to accessing and manipulating data in your workflows.

Stop And Error
General

This template is a hands-on, practical exam designed to test your understanding of the fundamental JSON data types. It's the perfect way to solidify your knowledge after learning the basics.

Stop And Error
General

Agendamiento. Uses n8n-nodes-evolution-api, redis, dataTable, executeWorkflowTrigger. Event-driven trigger; 60 nodes.

N8N Nodes Evolution Api, Redis, Data Table +2