{
  "name": "EAIR CRM Follow-up – Entwürfe",
  "nodes": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "name": "Schedule: Werktags 07:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -1500,
        -480
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000002",
      "name": "Webhook: Manueller Lauf",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -1500,
        -240
      ],
      "parameters": {
        "httpMethod": "POST",
        "path": "eair-crm-followup-manual",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000048",
      "name": "Code: Runtime-Konfiguration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1260,
        -480
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const config = {\"EAIR_CRM_SPREADSHEET_ID\":\"\",\"EAIR_CRM_LEADS_SHEET_ID\":\"\",\"EAIR_CRM_OPT_OUT_SHEET_ID\":\"\",\"EAIR_CRM_EVENTS_SHEET_ID\":\"\",\"EAIR_CRM_HOLIDAYS_SHEET_ID\":\"\",\"EAIR_CRM_RUNS_SHEET_ID\":\"\",\"EAIR_REPORT_RECIPIENT\":\"\",\"EAIR_GMAIL_DRAFT_LABEL_ID\":\"\",\"EAIR_VALIDATION_SPREADSHEET_ID\":\"\",\"EAIR_VALIDATION_TAB_NAME\":\"\"};\nconst eairTrigger = $input.all().some((item) => item.json?.headers) ? 'webhook' : 'schedule';\nreturn [{ json: { ...config, eairTrigger } }];"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000005",
      "name": "IF: Konfiguration vollständig",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -1040,
        -480
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000000501",
              "leftValue": "={{ (() => {\n    const textKeys = [\n      'EAIR_CRM_SPREADSHEET_ID', 'EAIR_REPORT_RECIPIENT',\n      'EAIR_VALIDATION_SPREADSHEET_ID', 'EAIR_VALIDATION_TAB_NAME', 'EAIR_GMAIL_DRAFT_LABEL_ID',\n    ];\n    const tabKeys = ['EAIR_CRM_LEADS_SHEET_ID', 'EAIR_CRM_OPT_OUT_SHEET_ID', 'EAIR_CRM_EVENTS_SHEET_ID', 'EAIR_CRM_HOLIDAYS_SHEET_ID', 'EAIR_CRM_RUNS_SHEET_ID'];\n    return textKeys.every((key) => typeof $json[key] === 'string' && $json[key].trim().length > 0)\n      && tabKeys.every((key) => /^\\d+$/.test(String($json[key] ?? '').trim()));\n  })() }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000006",
      "name": "Code: Run-Kontext",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -820,
        -480
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const trigger = $input.all().some((item) => item.json?.eairTrigger === 'webhook') ? 'webhook' : 'schedule';\nreturn [{ json: {\n  runId: 'run-' + $execution.id,\n  trigger,\n  startedAt: $now.setZone('Europe/Berlin').toISO(),\n} }];"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000007",
      "name": "IF: Webhook-Lauf",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -600,
        -480
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000000701",
              "leftValue": "={{ $json.trigger }}",
              "rightValue": "webhook",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000008",
      "name": "Respond: Lauf angenommen",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.4,
      "position": [
        -380,
        -600
      ],
      "parameters": {
        "respondWith": "text",
        "responseBody": "{\"accepted\":true}",
        "options": {
          "responseCode": 202,
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        }
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000009",
      "name": "Sheets: Leads laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -140,
        -480
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_LEADS_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: Unternehmen | Branche | Größe | Region | Segment | Triggersignal / beobachtetes Problem | Passendes EAIR-Einstiegsangebot | Zielrolle | Konkrete Kontaktperson | Zugangskanal | Problemstärke | KI-/Transformationsanlass | Ansprechpartner-Zugang | Umsetzungsfähigkeit | Angebotspassung | Budgetnähe | Beziehungsvorteil | Gesamtscore | Priorität | Begründung | Kontaktstatus | Letzter Kontakt | Nächster Schritt | Wiedervorlage | Einwände / Erkenntnisse | Quelle | Pipeline-Typ | Follow-up-Stufe | Terminstatus | Opt-out / keine weitere Ansprache | Lead-ID | E-Mail | Versuch"
    },
    {
      "id": "00000000-0000-4000-8000-000000000010",
      "name": "Sheets: Opt-outs laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        80,
        -480
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_OPT_OUT_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: type | value | reason | created_at | created_by"
    },
    {
      "id": "00000000-0000-4000-8000-000000000011",
      "name": "Sheets: Events laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        300,
        -480
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: event_id | run_id | lead_id | event_type | status | channel | template_key | subject_marker | gmail_draft_id | gmail_message_id | gmail_thread_id | occurred_at | details_json"
    },
    {
      "id": "00000000-0000-4000-8000-000000000012",
      "name": "Sheets: Feiertage laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        520,
        -480
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_HOLIDAYS_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: datum | bezeichnung | geltungsbereich"
    },
    {
      "id": "00000000-0000-4000-8000-000000000033",
      "name": "Sheets: Validierungstracker laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        700,
        -720
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_VALIDATION_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_VALIDATION_TAB_NAME }}",
          "mode": "name"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Read-only Header-Vertrag EAIR – Marktvalidierungs- & Interviewtracker / Gespräche!A1:Z1: Gespräch-ID | Datum | Unternehmen / Organisation | Segment | Pipeline-Typ | Gesprächspartner / Rolle | Branche / Kontext | Konkreter Anlass | Besprochenes Modul | Problemstärke 0–3 | Dringlichkeit 0–3 | Nutzenverständnis 0–3 | Vertrauen 0–3 | Modulpassung 0–3 | Preis-/Budgetnähe 0–3 | Sponsor/Entscheidung 0–3 | Umsetzungsfähigkeit 0–3 | Nächster Schritt 0–3 | Gesamtscore | Preisreaktion | Stärkster Einwand | Kaufsignal | Reaktionscodes | Nächster Schritt | Wiedervorlage | Interviewstatus"
    },
    {
      "id": "00000000-0000-4000-8000-000000000032",
      "name": "Gmail: Offene Entwürfe laden",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        700,
        -600
      ],
      "parameters": {
        "resource": "message",
        "operation": "getAll",
        "returnAll": true,
        "simple": true,
        "filters": {
          "labelIds": [
            "DRAFT"
          ],
          "q": "subject:[EAIR: newer_than:30d",
          "readStatus": "both"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000013",
      "name": "Gmail: Gesendete Nachrichten laden",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        740,
        -480
      ],
      "parameters": {
        "resource": "message",
        "operation": "getAll",
        "returnAll": true,
        "simple": true,
        "filters": {
          "labelIds": [
            "SENT"
          ],
          "q": "subject:[EAIR: newer_than:30d",
          "readStatus": "both"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": true,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000014",
      "name": "Code: Leads entscheiden",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        -480
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// domain-source-sha256:8f33e5e39ca1e8c2b4e5b8c4063e157580eafdc85300503464d5d423fb48c887\nconst EMBEDDED_DOMAIN_SOURCE = \"import { CRM_REFERENCE } from './reference-data.mjs';\\n\\nconst REQUIRED_FIELDS = [...CRM_REFERENCE.columns, ...CRM_REFERENCE.technicalFields]\\n  .filter((field) => field.roles.includes('required'))\\n  .map((field) => field.name);\\n\\nconst sequenceRules = CRM_REFERENCE.states\\n  .filter((state) => state.automationRule?.templateKey)\\n  .map((state) => ({ status: state.name, ...state.automationRule }));\\n\\nconst closureRule = CRM_REFERENCE.states.find((state) => state.automationRule?.closesSequence)?.automationRule;\\nconst closureEligibleStatuses = new Set([\\n  ...sequenceRules.map(({ status }) => status),\\n  CRM_REFERENCE.states.find((state) => state.automationRule?.closesSequence)?.name,\\n].filter(Boolean));\\n\\n/**\\n * Runtime inputs required to evaluate one lead without external calls.\\n *\\n * @typedef {object} RunContext\\n * @property {string} today ISO date in the workflow timezone.\\n * @property {Set<string>} holidays ISO dates that are not business days.\\n * @property {object[]} leads All currently loaded leads, used only for duplicate-email detection.\\n * @property {{ type: 'email' | 'domain', value: string }[]} optOuts Append-only absolute contact blocks.\\n * @property {Set<string>} openDraftLeadIds Lead IDs that already have an unsent draft.\\n * @property {object[]} manualSignals Append-only, lifecycle-scoped manual signal events.\\n * @property {string} [sequenceEpoch] Explicit append-only epoch of the currently active sequence.\\n * @property {string} [sequenceStartedAt] ISO timestamp of the active sequence epoch's append-only start.\\n * @property {string} [now] Current ISO timestamp used for temporal signal validation.\\n */\\n\\n/**\\n * Deterministic outcome for whether the workflow may prepare a draft.\\n *\\n * @typedef {object} LeadDecision\\n * @property {'create_draft' | 'close_sequence' | 'skip'} action\\n * @property {string} reason Machine-readable German explanation for the selected outcome.\\n * @property {string | undefined} [templateKey] Canonical template key when a draft is allowed.\\n * @property {object | undefined} [statePatch] CRM fields to update when the sequence is closed.\\n */\\n\\n/**\\n * Deterministic outcome of comparing a draft event with manually sent messages.\\n *\\n * @typedef {object} DraftDecision\\n * @property {'advance_state' | 'escalate' | 'wait'} action\\n * @property {string} reason Machine-readable German explanation for the selected outcome.\\n * @property {object | undefined} [statePatch] CRM fields to update after a confirmed manual send.\\n */\\n\\nconst text = (value) => (typeof value === 'string' ? value.trim() : '');\\n\\nconst eventDetails = (event) => {\\n  try { return JSON.parse(event?.details_json || '{}'); } catch { return {}; }\\n};\\n\\n/** Returns the deterministic lifecycle scope for mutations of the lead's current sequence state. */\\nexport function sequenceLifecycleId(lead, sequenceEpoch = 'initial') {\\n  return ['sequence', text(lead?.['Lead-ID']), 'epoch', text(sequenceEpoch) || 'initial'].join(':');\\n}\\n\\nconst isoTimestamp = (value) => {\\n  const iso = text(value);\\n  const match = iso.match(/^(\\\\d{4})-(\\\\d{2})-(\\\\d{2})T(\\\\d{2}):(\\\\d{2}):(\\\\d{2})(\\\\.\\\\d{1,3})?(Z|([+-])(\\\\d{2}):(\\\\d{2}))$/u);\\n  if (!match) return null;\\n  const [, yearText, monthText, dayText, hourText, minuteText, secondText,\\n    fraction = '', zone, offsetSign = '+', offsetHourText = '00', offsetMinuteText = '00'] = match;\\n  const [year, month, day, hour, minute, second, offsetHour, offsetMinute] = [\\n    yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText,\\n  ].map(Number);\\n  if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59\\n    || offsetHour > 23 || offsetMinute > 59) return null;\\n  const parsed = Date.parse(iso);\\n  if (!Number.isFinite(parsed)) return null;\\n  const signedOffsetMinutes = zone === 'Z'\\n    ? 0\\n    : (offsetSign === '+' ? 1 : -1) * ((offsetHour * 60) + offsetMinute);\\n  const localRoundtrip = new Date(parsed + (signedOffsetMinutes * 60_000));\\n  const millisecond = Number(fraction.slice(1).padEnd(3, '0') || 0);\\n  if (localRoundtrip.getUTCFullYear() !== year || localRoundtrip.getUTCMonth() + 1 !== month\\n    || localRoundtrip.getUTCDate() !== day || localRoundtrip.getUTCHours() !== hour\\n    || localRoundtrip.getUTCMinutes() !== minute || localRoundtrip.getUTCSeconds() !== second\\n    || localRoundtrip.getUTCMilliseconds() !== millisecond) return null;\\n  return parsed;\\n};\\n\\nconst hasActiveManualSignal = (manualSignals, {\\n  lifecycleId, leadId, sequenceEpoch, notBefore, now,\\n}) => {\\n  const lowerBound = isoTimestamp(notBefore);\\n  const currentTime = isoTimestamp(now);\\n  if (currentTime === null || (text(notBefore) && lowerBound === null)) return false;\\n  return (manualSignals ?? []).some((signal) => {\\n    if (signal?.event_type !== 'manual_signal' || text(signal?.lead_id) !== text(leadId)) return false;\\n    const details = eventDetails(signal);\\n    if (text(details.sequence_epoch) !== text(sequenceEpoch)\\n      || text(details.lifecycle_id) !== lifecycleId) return false;\\n    const occurredAt = isoTimestamp(signal?.occurred_at);\\n    if (occurredAt === null || (lowerBound !== null && occurredAt < lowerBound)) return false;\\n    if (currentTime !== null && occurredAt > currentTime) return false;\\n    if (!Object.hasOwn(details, 'expires_at')) return true;\\n    const expiresAt = isoTimestamp(details.expires_at);\\n    return expiresAt !== null && (currentTime === null || expiresAt >= currentTime);\\n  });\\n};\\n\\n/** @param {unknown} value @returns {string} */\\nexport function normalizeEmail(value) {\\n  return text(value).toLowerCase();\\n}\\n\\n/** @param {unknown} value @returns {string} */\\nexport function emailDomain(value) {\\n  const email = normalizeEmail(value);\\n  const at = email.lastIndexOf('@');\\n  return at > 0 && at < email.length - 1 ? email.slice(at + 1) : '';\\n}\\n\\nconst dateAtUtc = (isoDate) => {\\n  if (typeof isoDate !== 'string' || !/^\\\\d{4}-\\\\d{2}-\\\\d{2}$/.test(isoDate)) {\\n    throw new TypeError('ISO-Datum erwartet');\\n  }\\n\\n  const [year, month, day] = isoDate.split('-').map(Number);\\n  const date = new Date(Date.UTC(year, month - 1, day));\\n  if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {\\n    throw new RangeError('Ungültiges ISO-Datum');\\n  }\\n  return date;\\n};\\n\\nconst isoDate = (date) => date.toISOString().slice(0, 10);\\n\\nconst isBusinessDay = (date, holidays) => {\\n  const day = date.getUTCDay();\\n  return day !== 0 && day !== 6 && !holidays.has(isoDate(date));\\n};\\n\\n/**\\n * Adds weekdays while skipping Saturday, Sunday, and explicitly supplied ISO holidays.\\n * @param {string} isoDateValue\\n * @param {number} days\\n * @param {Set<string>} holidays\\n * @returns {string}\\n */\\nexport function addBusinessDays(isoDateValue, days, holidays) {\\n  if (!Number.isInteger(days) || days < 0) throw new RangeError('Nichtnegative ganze Werktage erwartet');\\n  if (!(holidays instanceof Set)) throw new TypeError('Feiertage als Set erwartet');\\n\\n  const date = dateAtUtc(isoDateValue);\\n  let remaining = days;\\n  while (remaining > 0) {\\n    date.setUTCDate(date.getUTCDate() + 1);\\n    if (isBusinessDay(date, holidays)) remaining -= 1;\\n  }\\n  return isoDate(date);\\n}\\n\\nconst businessDaysBetween = (from, to, holidays) => {\\n  const date = dateAtUtc(from);\\n  const end = dateAtUtc(to);\\n  let count = 0;\\n  while (date < end) {\\n    date.setUTCDate(date.getUTCDate() + 1);\\n    if (isBusinessDay(date, holidays)) count += 1;\\n  }\\n  return count;\\n};\\n\\n/** @param {object} lead @returns {string[]} */\\nexport function validateRequiredFields(lead) {\\n  return REQUIRED_FIELDS.filter((field) => !text(lead?.[field]));\\n}\\n\\nconst renderTemplateText = (sourceText, mappings, lead, missing) => sourceText.replace(/\\\\[([^\\\\]]+)\\\\]/gu, (_, token) => {\\n  const mapping = mappings[token];\\n  if (!mapping) throw new TypeError(`Nicht dokumentierter Vorlagenplatzhalter: ${token}`);\\n  const value = mapping.fields.map((field) => text(lead?.[field])).find(Boolean)\\n    || text(lead?.[mapping.missingLabel]);\\n  if (value) return value;\\n  if (!missing.includes(mapping.missingLabel)) missing.push(mapping.missingLabel);\\n  return `[[FEHLT: ${mapping.missingLabel}]]`;\\n});\\n\\n/**\\n * Renders one canonical template without inventing facts: missing placeholder values remain visible.\\n * @param {string} templateKey\\n * @param {object} lead\\n * @returns {{ subject: string, body: string, missing: string[] }}\\n */\\nexport function renderTemplate(templateKey, lead) {\\n  const template = CRM_REFERENCE.templates.find((entry) => entry.key === templateKey);\\n  if (!template) throw new RangeError(`Unbekannte Vorlage: ${templateKey}`);\\n  if (!template.automatic || !template.subjectTemplate || !template.bodyTemplate || !template.placeholderMappings) {\\n    throw new RangeError(`Vorlage ist nicht für automatische Entwürfe freigegeben: ${templateKey}`);\\n  }\\n\\n  const missing = [];\\n  return {\\n    subject: renderTemplateText(template.subjectTemplate, template.placeholderMappings, lead, missing),\\n    body: renderTemplateText(template.bodyTemplate, template.placeholderMappings, lead, missing),\\n    missing,\\n  };\\n}\\n\\nconst isOptedOut = (lead, optOuts) => {\\n  const localOptOut = text(lead?.['Opt-out / keine weitere Ansprache']).toLowerCase();\\n  if (localOptOut === 'ja' || localOptOut === 'true') return true;\\n\\n  const email = normalizeEmail(lead?.['E-Mail']);\\n  const domain = emailDomain(email);\\n  return (optOuts ?? []).some((entry) => (\\n    (entry.type === 'email' && normalizeEmail(entry.value) === email)\\n    || (entry.type === 'domain' && text(entry.value).toLowerCase() === domain)\\n  ));\\n};\\n\\nconst isDuplicate = (lead, leads) => {\\n  const email = normalizeEmail(lead?.['E-Mail']);\\n  if (!email) return false;\\n  const domain = emailDomain(email);\\n  const validDomain = domain.length <= 253 && domain.includes('.')\\n    && domain.split('.').every((label) => (\\n      label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)\\n    ));\\n  const leadId = text(lead?.['Lead-ID']);\\n  return (leads ?? []).some((candidate) => {\\n    if (candidate === lead) return false;\\n    const candidateLeadId = text(candidate?.['Lead-ID']);\\n    if (leadId && candidateLeadId === leadId) return false;\\n    const candidateEmail = normalizeEmail(candidate?.['E-Mail']);\\n    if (!candidateEmail) return false;\\n    if (candidateEmail === email) return true;\\n    return validDomain && emailDomain(candidateEmail) === domain;\\n  });\\n};\\n\\nconst isDue = (lead, today) => {\\n  const dueDate = text(lead?.Wiedervorlage);\\n  return Boolean(dueDate) && dueDate <= today;\\n};\\n\\n/**\\n * Chooses at most one draft action after applying all CRM gates in their required order.\\n * @param {object} lead\\n * @param {RunContext} context\\n * @returns {LeadDecision}\\n */\\nexport function planLeadAction(lead, context) {\\n  if (validateRequiredFields(lead).length > 0) return { action: 'skip', reason: 'Pflichtfeld-Lücke' };\\n  if (isDuplicate(lead, context?.leads)) return { action: 'skip', reason: 'Dublette' };\\n  if (isOptedOut(lead, context?.optOuts)) return { action: 'skip', reason: 'Opt-out' };\\n\\n  const attempt = Number(lead?.Versuch);\\n  const sequenceEpoch = text(context?.sequenceEpoch) || 'initial';\\n  if (hasActiveManualSignal(context?.manualSignals, {\\n    lifecycleId: sequenceLifecycleId(lead, sequenceEpoch),\\n    leadId: lead?.['Lead-ID'],\\n    sequenceEpoch,\\n    notBefore: context?.sequenceStartedAt,\\n    now: context?.now || `${context?.today ?? ''}T12:00:00Z`,\\n  })) {\\n    return { action: 'skip', reason: 'Neues manuelles Signal' };\\n  }\\n  if (closureRule && attempt >= closureRule.attempt && closureEligibleStatuses.has(text(lead?.Kontaktstatus))) {\\n    if (!isDue(lead, context?.today ?? '')) return { action: 'skip', reason: 'Nicht fällig' };\\n    return {\\n      action: 'close_sequence',\\n      reason: 'Versuchslimit',\\n      statePatch: {\\n        Kontaktstatus: closureRule.nextState,\\n        Wiedervorlage: '',\\n        'Einwände / Erkenntnisse': 'Keine Reaktion nach drei Versuchen; neues manuelles Signal erforderlich',\\n      },\\n    };\\n  }\\n  if (context?.openDraftLeadIds?.has(text(lead?.['Lead-ID']))) return { action: 'skip', reason: 'Offener Entwurf' };\\n\\n  const rule = sequenceRules.find((entry) => entry.status === lead?.Kontaktstatus && entry.attempt === attempt);\\n  if (!rule) return { action: 'skip', reason: 'Zustandsregel' };\\n  if (!isDue(lead, context?.today ?? '')) return { action: 'skip', reason: 'Nicht fällig' };\\n\\n  return { action: 'create_draft', reason: 'Fällig', templateKey: rule.templateKey };\\n}\\n\\nconst matchesSentMessage = (event, message) => {\\n  const messageId = text(event?.gmail_message_id);\\n  if (messageId) {\\n    return [message?.id, message?.messageId, message?.gmail_message_id]\\n      .some((value) => text(value) === messageId);\\n  }\\n\\n  const draftId = text(event?.gmail_draft_id);\\n  return Boolean(draftId && [message?.draftId, message?.draft_id, message?.gmail_draft_id]\\n    .some((value) => text(value) === draftId));\\n};\\n\\n/**\\n * Reconciles one draft event only after a matching manual-send record is observed.\\n * @param {object} event\\n * @param {object[]} sentMessages\\n * @param {string} today ISO date in the workflow timezone.\\n * @param {Set<string>} holidays\\n * @param {object} liveLead freshly loaded lead row used to guard manual signals and state drift.\\n * @param {{ type: 'email' | 'domain', value: string }[]} [optOuts]\\n * @param {object[]} [manualSignals] append-only lifecycle-scoped manual signal events.\\n * @param {string} [now] retained for backwards-compatible workflow invocation.\\n * @returns {DraftDecision}\\n */\\nexport function reconcileDraft(\\n  event, sentMessages, today, holidays, liveLead, optOuts = [], manualSignals = [],\\n  now = `${today}T12:00:00Z`,\\n) {\\n  const wasSent = (sentMessages ?? []).some((message) => matchesSentMessage(event, message));\\n  if (wasSent) {\\n    const templateKey = text(event?.template_key || event?.templateKey);\\n    const rule = sequenceRules.find((entry) => entry.templateKey === templateKey);\\n    if (!rule) return { action: 'wait', reason: 'Unbekannte Vorlage' };\\n\\n    const lifecycleId = text(eventDetails(event).lifecycle_id)\\n      || text(event?.event_id).replace(/:(?:reserved|created)$/u, '');\\n    const sequenceEpoch = text(eventDetails(event).sequence_epoch) || 'initial';\\n    if (hasActiveManualSignal(manualSignals, {\\n      lifecycleId,\\n      leadId: event?.lead_id || liveLead?.['Lead-ID'],\\n      sequenceEpoch,\\n      notBefore: event?.occurred_at || event?.created_at,\\n      now,\\n    })) return { action: 'wait', reason: 'Neues manuelles Signal schützt Lebenszyklus' };\\n\\n    const stateIsStillEligible = text(liveLead?.Kontaktstatus) === rule.status\\n      && Number(liveLead?.Versuch) === rule.attempt\\n      && !isOptedOut(liveLead, optOuts);\\n    if (!stateIsStillEligible) return { action: 'wait', reason: 'Manuelles Signal schützt Zustand' };\\n\\n    const statePatch = {\\n      Kontaktstatus: rule.nextState,\\n      Versuch: rule.nextAttempt,\\n      'Letzter Kontakt': today,\\n    };\\n    if (rule.waitBusinessDays !== null) statePatch.Wiedervorlage = addBusinessDays(today, rule.waitBusinessDays, holidays);\\n    return { action: 'advance_state', reason: 'Manuell versendet', statePatch };\\n  }\\n\\n  const occurredAt = text(event?.occurred_at || event?.created_at);\\n  if (isoTimestamp(occurredAt) === null) return { action: 'wait', reason: 'Entwurf offen' };\\n  const occurredOn = occurredAt.slice(0, 10);\\n  if (occurredOn && businessDaysBetween(occurredOn, today, holidays) >= 4) {\\n    return { action: 'escalate', reason: 'Entwurf liegt' };\\n  }\\n  return { action: 'wait', reason: 'Entwurf offen' };\\n}\\n\";\nconst CRM_REFERENCE = {\"source\":{\"statusLogic\":\"CRM-Statuslogik!A2:H19\",\"customerHeaders\":\"Zielkundenliste!A1:AD1\",\"process\":\"EAIR – CRM-, Termin- & Follow-up-Prozess, Abschnitte 2–10\",\"design\":\"CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\",\"templates\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},\"states\":[{\"name\":\"Recherche\",\"meaning\":\"Unternehmen ist aufgenommen, aber noch nicht kontaktfähig qualifiziert.\",\"requiredAction\":\"Signal, Größe, Rolle und Quelle prüfen.\",\"deadline\":\"keine feste Frist\",\"nextStates\":\"Qualifiziert / Zurückgestellt\",\"followUpRequired\":\"Nein\",\"pipeline\":\"beide\",\"closureRule\":\"Keine Ansprache erlaubt.\",\"event\":\"Aufnahme zur Qualifizierung\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Qualifiziert\",\"meaning\":\"Problem, Zielrolle und Einstieg sind grundsätzlich plausibel.\",\"requiredAction\":\"Kontaktperson und aktuellen Anlass verifizieren.\",\"deadline\":\"innerhalb 5 Werktagen\",\"nextStates\":\"Freigegeben / Zurückgestellt\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Noch keine Ansprache ohne Freigabe.\",\"event\":\"Qualifikation bestätigt\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Freigegeben\",\"meaning\":\"Ansprache ist nach Einzelfallprüfung zulässig.\",\"requiredAction\":\"Personalisierte Nachricht final prüfen und dokumentieren.\",\"deadline\":\"innerhalb 3 Werktagen\",\"nextStates\":\"Kontaktiert 1\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Freigabe verfällt bei veraltetem Signal.\",\"event\":\"Fällige, vollständige und nicht gesperrte Freigabe bei Versuch 0\",\"automation\":\"Teilautomatisiert: Erstkontakt-Entwurf vorbereiten; Versand manuell.\",\"automationRule\":{\"attempt\":0,\"templateKey\":\"erstkontakt_a1_a2\",\"nextState\":\"Kontaktiert 1\",\"nextAttempt\":1,\"waitBusinessDays\":7},\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Kontaktiert 1\",\"meaning\":\"Erstkontakt wurde versendet.\",\"requiredAction\":\"Letzten Kontakt, Vorlage und Wiedervorlage dokumentieren.\",\"deadline\":\"5–7 Werktage\",\"nextStates\":\"Antwort / Follow-up 1\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Kein paralleler Druck über mehrere Kanäle.\",\"event\":\"Fälliger Erstkontakt ohne Antwort bei Versuch 1\",\"automation\":\"Teilautomatisiert: Follow-up-1-Entwurf vorbereiten; Versand manuell.\",\"automationRule\":{\"attempt\":1,\"templateKey\":\"followup_1\",\"nextState\":\"Follow-up 1\",\"nextAttempt\":2,\"waitBusinessDays\":10},\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Follow-up 1\",\"meaning\":\"Erstes sachliches Nachfassen ohne Reaktion.\",\"requiredAction\":\"Zusätzlichen Nutzenaspekt nennen und Ausstieg ermöglichen.\",\"deadline\":\"7–10 Werktage\",\"nextStates\":\"Antwort / Follow-up 2\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Nur bei weiterhin aktuellem Anlass.\",\"event\":\"Fälliges erstes Follow-up ohne Antwort bei Versuch 2\",\"automation\":\"Teilautomatisiert: Abschlussentwurf vorbereiten; Versand manuell.\",\"automationRule\":{\"attempt\":2,\"templateKey\":\"abschluss\",\"nextState\":\"Follow-up 2\",\"nextAttempt\":3,\"waitBusinessDays\":7},\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Follow-up 2\",\"meaning\":\"Letzte Nachricht der Sequenz.\",\"requiredAction\":\"Nach sieben Werktagen und nur bei weiterhin fehlender Reaktion die Sequenz respektvoll schließen.\",\"deadline\":\"7 Werktage nach Abschlussnachricht\",\"nextStates\":\"Antwort / Zurückgestellt / Abgelehnt\",\"followUpRequired\":\"nur bei spätem Anlass\",\"pipeline\":\"beide\",\"closureRule\":\"Keine weitere Ansprache ohne neues Signal.\",\"event\":\"Fällige Wiedervorlage nach mindestens drei Versuchen ohne neues manuelles Signal\",\"automation\":\"Teilautomatisiert: Erst bei fälliger Wiedervorlage ohne Entwurf nach Zurückgestellt führen.\",\"automationRule\":{\"attempt\":3,\"nextState\":\"Zurückgestellt\",\"closesSequence\":true},\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Antwort\",\"meaning\":\"Es liegt eine Reaktion vor.\",\"requiredAction\":\"Relevanz, Rolle und nächsten Schritt klären.\",\"deadline\":\"innerhalb 1 Werktag\",\"nextStates\":\"Termin vereinbart / Zurückgestellt / Abgelehnt\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Antwortinhalt sachlich dokumentieren.\",\"event\":\"Reaktion eingegangen\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Termin vereinbart\",\"meaning\":\"Gespräch ist verbindlich geplant.\",\"requiredAction\":\"Ziel, Teilnehmende und Ausgangsfrage bestätigen.\",\"deadline\":\"spätestens 1 Werktag vorher\",\"nextStates\":\"Gespräch geführt / Neu terminieren\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Terminstatus separat pflegen.\",\"event\":\"Termin verbindlich geplant\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Gespräch geführt\",\"meaning\":\"Qualifiziertes Gespräch wurde durchgeführt.\",\"requiredAction\":\"CRM-Protokoll und Zusammenfassung am selben oder folgenden Werktag.\",\"deadline\":\"1 Werktag\",\"nextStates\":\"Unterlagen versendet / Angebot in Vorbereitung / Zurückgestellt\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Kein Angebot ohne ausreichende Entscheidungsreife.\",\"event\":\"Qualifiziertes Gespräch durchgeführt\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Unterlagen versendet\",\"meaning\":\"Passende Unterlage wurde nach Gespräch gesendet.\",\"requiredAction\":\"Konkreten Follow-up-Termin vereinbaren.\",\"deadline\":\"nach Vereinbarung, sonst 5–7 Werktage\",\"nextStates\":\"Antwort / Angebot in Vorbereitung / Zurückgestellt\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Nur relevante Unterlagen versenden.\",\"event\":\"Vereinbarte Unterlagen versendet\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Angebot in Vorbereitung\",\"meaning\":\"Scope und kommerzielle Grundlage sind ausreichend geklärt.\",\"requiredAction\":\"Angebot mit Annahmen, Mitwirkung, Abnahme und Gültigkeit erstellen.\",\"deadline\":\"nach vereinbartem Termin\",\"nextStates\":\"Angebot versendet\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Offene Voraussetzungen sichtbar halten.\",\"event\":\"Scope und kommerzielle Grundlage geklärt\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Angebot versendet\",\"meaning\":\"Verbindliches Angebot liegt beim Kunden.\",\"requiredAction\":\"Versand dokumentieren; Erinnerung terminieren.\",\"deadline\":\"5–7 Werktage\",\"nextStates\":\"Entscheidung offen / Gewonnen / Verloren\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Höchstens eine weitere Abschlussnachricht.\",\"event\":\"Verbindliches Angebot versendet\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Entscheidung offen\",\"meaning\":\"Interne Entscheidung des Kunden läuft.\",\"requiredAction\":\"Entscheidungstermin und fehlende Punkte klären.\",\"deadline\":\"nach Vereinbarung\",\"nextStates\":\"Gewonnen / Verloren / Zurückgestellt\",\"followUpRequired\":\"Ja\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Keine künstliche Dringlichkeit.\",\"event\":\"Interne Kundenentscheidung läuft\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Gewonnen\",\"meaning\":\"Beauftragung oder verbindlicher Pilotprozess startet.\",\"requiredAction\":\"Übergabe in Scoping, Angebot oder Arbeitspaket 4.9 dokumentieren.\",\"deadline\":\"sofort\",\"nextStates\":\"Endstatus\",\"followUpRequired\":\"Nein\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Vertriebsprozess abgeschlossen.\",\"event\":\"Beauftragung oder verbindlicher Pilotprozess startet\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Verloren\",\"meaning\":\"Kunde entscheidet sich gegen das Vorhaben oder EAIR.\",\"requiredAction\":\"Grund und Erkenntnis dokumentieren.\",\"deadline\":\"sofort\",\"nextStates\":\"Endstatus\",\"followUpRequired\":\"Nein\",\"pipeline\":\"Direktkunde\",\"closureRule\":\"Nur bei neuem Signal wieder öffnen.\",\"event\":\"Entscheidung gegen Vorhaben oder EAIR\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Zurückgestellt\",\"meaning\":\"Thema ist später möglich, aktuell aber nicht aktiv.\",\"requiredAction\":\"Konkreten Wiedervorlagegrund und Termin setzen.\",\"deadline\":\"individuell\",\"nextStates\":\"Recherche / Qualifiziert\",\"followUpRequired\":\"Ja\",\"pipeline\":\"beide\",\"closureRule\":\"Ohne Anlass keine automatische Reaktivierung.\",\"event\":\"Thema aktuell nicht aktiv\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Abgelehnt\",\"meaning\":\"Keine Relevanz oder ausdrückliche Ablehnung.\",\"requiredAction\":\"Grund dokumentieren, keine weitere Sequenz.\",\"deadline\":\"sofort\",\"nextStates\":\"Endstatus\",\"followUpRequired\":\"Nein\",\"pipeline\":\"beide\",\"closureRule\":\"Ablehnung respektieren.\",\"event\":\"Keine Relevanz oder ausdrückliche Ablehnung\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"},{\"name\":\"Opt-out\",\"meaning\":\"Keine weitere Ansprache gewünscht.\",\"requiredAction\":\"Opt-out-Feld auf Ja setzen und alle Wiedervorlagen entfernen.\",\"deadline\":\"sofort\",\"nextStates\":\"Endstatus\",\"followUpRequired\":\"Nein\",\"pipeline\":\"beide\",\"closureRule\":\"Absolute Kontaktsperre.\",\"event\":\"Keine weitere Ansprache gewünscht\",\"automation\":\"Manuell\",\"source\":\"CRM-Statuslogik!A2:H19; CRM Approval & Follow-up Orchestrator — Design, Abschnitt 4\"}],\"columns\":[{\"number\":1,\"name\":\"Unternehmen\",\"roles\":[\"required\",\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":2,\"name\":\"Branche\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":3,\"name\":\"Größe\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":4,\"name\":\"Region\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":5,\"name\":\"Segment\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":6,\"name\":\"Triggersignal / beobachtetes Problem\",\"roles\":[\"required\",\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":7,\"name\":\"Passendes EAIR-Einstiegsangebot\",\"roles\":[\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":8,\"name\":\"Zielrolle\",\"roles\":[\"required\",\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":9,\"name\":\"Konkrete Kontaktperson\",\"roles\":[\"required\",\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":10,\"name\":\"Zugangskanal\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":11,\"name\":\"Problemstärke\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":12,\"name\":\"KI-/Transformationsanlass\",\"roles\":[\"placeholder\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":13,\"name\":\"Ansprechpartner-Zugang\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":14,\"name\":\"Umsetzungsfähigkeit\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":15,\"name\":\"Angebotspassung\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":16,\"name\":\"Budgetnähe\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":17,\"name\":\"Beziehungsvorteil\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":18,\"name\":\"Gesamtscore\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":19,\"name\":\"Priorität\",\"roles\":[\"required\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":20,\"name\":\"Begründung\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":21,\"name\":\"Kontaktstatus\",\"roles\":[\"required\",\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":22,\"name\":\"Letzter Kontakt\",\"roles\":[\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":23,\"name\":\"Nächster Schritt\",\"roles\":[\"required\",\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":24,\"name\":\"Wiedervorlage\",\"roles\":[\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":25,\"name\":\"Einwände / Erkenntnisse\",\"roles\":[\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":26,\"name\":\"Quelle\",\"roles\":[\"required\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":27,\"name\":\"Pipeline-Typ\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":28,\"name\":\"Follow-up-Stufe\",\"roles\":[\"unused\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":29,\"name\":\"Terminstatus\",\"roles\":[\"read-only\"],\"source\":\"Zielkundenliste!A1:AD1\"},{\"number\":30,\"name\":\"Opt-out / keine weitere Ansprache\",\"roles\":[\"blocker\"],\"source\":\"Zielkundenliste!A1:AD1\"}],\"technicalFields\":[{\"name\":\"Lead-ID\",\"purpose\":\"Stabile technische Identifikation eines Demo-Leads.\",\"roles\":[\"blocker\"]},{\"name\":\"E-Mail\",\"purpose\":\"Dedizierte Empfängeradresse für Sperr- und Dublettenprüfung.\",\"roles\":[\"required\",\"blocker\"]},{\"name\":\"Versuch\",\"purpose\":\"Numerischer Zähler der höchstens drei Kontaktversuche.\",\"roles\":[\"blocker\"]}],\"columnRoleLabels\":{\"required\":\"Pflichtfeld\",\"blocker\":\"Sperrkriterium\",\"placeholder\":\"Vorlagenplatzhalter\",\"read-only\":\"Nur lesend\",\"unused\":\"Nicht verwendet\"},\"templates\":[{\"key\":\"erstkontakt_a1_a2\",\"title\":\"Personalisierte Erstkontakt-E-Mail – A1/A2\",\"section\":\"Abschnitt 3\",\"automatic\":true,\"placeholders\":[\"Name\",\"Unternehmen\",\"Triggersignal\",\"Entscheidungsfrage\",\"Passendes EAIR-Einstiegsangebot\"],\"subjectTemplate\":\"Einordnung Ihrer KI-Initiative [Thema]\",\"bodyTemplate\":\"Guten Tag [Name],\\n\\nöffentlich sichtbar ist, dass [Unternehmen] [konkretes, belegtes Signal]. Daraus kann sich die Frage ergeben, wie [konkrete Entscheidungsfrage], bevor weitere Umsetzung oder Skalierung erfolgt.\\n\\nMit EAIR unterstütze ich Unternehmen dabei, KI-Vorhaben über Strategie, Wirtschaftlichkeit, Technologie, Governance und Organisation hinweg entscheidbar zu machen – von der Ausgangslage bis zu einem kontrollierten Pilot- oder Skalierungspfad.\\n\\nFalls dieses Thema bei Ihnen aktuell relevant ist, würde ich mich über einen 20- bis 30-minütigen Austausch freuen. Ziel wäre zunächst nur zu prüfen, ob [passendes EAIR-Modul beziehungsweise Entscheidungsfrage] für Ihre Situation sinnvoll ist.\\n\\nFreundliche Grüße\\nBjörn Riemer\\n[Kontaktdaten]\",\"placeholderMappings\":{\"Thema\":{\"fields\":[\"KI-/Transformationsanlass\",\"Triggersignal / beobachtetes Problem\"],\"missingLabel\":\"Thema\",\"sourceSchemaGap\":false},\"Name\":{\"fields\":[\"Konkrete Kontaktperson\"],\"missingLabel\":\"Name\",\"sourceSchemaGap\":false},\"Unternehmen\":{\"fields\":[\"Unternehmen\"],\"missingLabel\":\"Unternehmen\",\"sourceSchemaGap\":false},\"konkretes, belegtes Signal\":{\"fields\":[\"Triggersignal / beobachtetes Problem\"],\"missingLabel\":\"Triggersignal\",\"sourceSchemaGap\":false},\"konkrete Entscheidungsfrage\":{\"fields\":[],\"missingLabel\":\"Entscheidungsfrage\",\"sourceSchemaGap\":true},\"passendes EAIR-Modul beziehungsweise Entscheidungsfrage\":{\"fields\":[\"Passendes EAIR-Einstiegsangebot\"],\"missingLabel\":\"Passendes EAIR-Einstiegsangebot\",\"sourceSchemaGap\":false},\"Kontaktdaten\":{\"fields\":[],\"missingLabel\":\"Kontaktdaten\",\"sourceSchemaGap\":true}},\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"erstkontakt_unklarer_ansprechpartner\",\"title\":\"Erstkontakt-E-Mail bei unklarem Ansprechpartner\",\"section\":\"Abschnitt 4\",\"automatic\":false,\"placeholders\":[\"Name\",\"Unternehmen\",\"Triggersignal\",\"Zielrolle\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"linkedin_kontaktanfrage\",\"title\":\"LinkedIn-Kontaktanfrage\",\"section\":\"Abschnitt 5\",\"automatic\":false,\"placeholders\":[\"Name\",\"Zielrolle\",\"Unternehmen\",\"Triggersignal\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"linkedin_nach_annahme\",\"title\":\"LinkedIn-Nachricht nach Annahme\",\"section\":\"Abschnitt 6\",\"automatic\":false,\"placeholders\":[\"Name\",\"Unternehmen\",\"Triggersignal\",\"Entscheidungsfrage\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"netzwerkempfehlung\",\"title\":\"Netzwerkempfehlung – Bitte an gemeinsamen Kontakt\",\"section\":\"Abschnitt 7\",\"automatic\":false,\"placeholders\":[\"Name\",\"Unternehmen\",\"Triggersignal\",\"Zielrolle\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"einfuehrungsnachricht_empfehlungsgeber\",\"title\":\"Einführungsnachricht des Empfehlungsgebers\",\"section\":\"Abschnitt 8\",\"automatic\":false,\"placeholders\":[\"Konkrete Kontaktperson\",\"Unternehmen\",\"Thema\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"multiplikator_expertenansprache\",\"title\":\"Multiplikator- oder Expertenansprache\",\"section\":\"Abschnitt 9\",\"automatic\":false,\"placeholders\":[\"Name\",\"Thema\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"followup_1\",\"title\":\"Kurzes Follow-up nach 5 bis 7 Werktagen\",\"section\":\"Abschnitt 10\",\"automatic\":true,\"placeholders\":[\"Name\",\"Thema\",\"Entscheidungsfrage\"],\"subjectTemplate\":\"Rückfrage zu [Thema]\",\"bodyTemplate\":\"Guten Tag [Name],\\n\\nich möchte kurz auf meine Nachricht zu [konkretes Thema] zurückkommen. Neben der technischen Umsetzung ist aus meiner Sicht besonders die Frage relevant, wie [zusätzlicher konkreter Entscheidungsaspekt] belastbar geklärt wird.\\n\\nFalls das aktuell in Ihrem Verantwortungsbereich liegt, freue ich mich über einen kurzen Austausch. Sollte das Thema derzeit nicht relevant sein oder eine andere Person zuständig sein, genügt eine kurze Rückmeldung.\\n\\nFreundliche Grüße\\nBjörn Riemer\",\"placeholderMappings\":{\"Thema\":{\"fields\":[\"KI-/Transformationsanlass\",\"Triggersignal / beobachtetes Problem\"],\"missingLabel\":\"Thema\",\"sourceSchemaGap\":false},\"Name\":{\"fields\":[\"Konkrete Kontaktperson\"],\"missingLabel\":\"Name\",\"sourceSchemaGap\":false},\"konkretes Thema\":{\"fields\":[\"Triggersignal / beobachtetes Problem\",\"KI-/Transformationsanlass\"],\"missingLabel\":\"Thema\",\"sourceSchemaGap\":false},\"zusätzlicher konkreter Entscheidungsaspekt\":{\"fields\":[],\"missingLabel\":\"Entscheidungsfrage\",\"sourceSchemaGap\":true}},\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"abschluss\",\"title\":\"Letzte Nachricht der Sequenz\",\"section\":\"Abschnitt 11\",\"automatic\":true,\"placeholders\":[\"Name\",\"Thema\"],\"subjectTemplate\":\"Abschluss meiner Anfrage zu [Thema]\",\"bodyTemplate\":\"Guten Tag [Name],\\n\\nich schließe meine Anfrage zu [Thema] zunächst ab, da ich keine Rückmeldung erhalten habe. Ich gehe davon aus, dass das Thema aktuell nicht priorisiert wird oder nicht in Ihren Verantwortungsbereich fällt.\\n\\nSollte sich zu einem späteren Zeitpunkt eine konkrete Frage zu Priorisierung, Pilot, Governance oder Skalierung ergeben, können wir gern daran anknüpfen. Ohne neues Signal erfolgt keine weitere Ansprache.\\n\\nFreundliche Grüße\\nBjörn Riemer\",\"placeholderMappings\":{\"Thema\":{\"fields\":[\"KI-/Transformationsanlass\",\"Triggersignal / beobachtetes Problem\"],\"missingLabel\":\"Thema\",\"sourceSchemaGap\":false},\"Name\":{\"fields\":[\"Konkrete Kontaktperson\"],\"missingLabel\":\"Name\",\"sourceSchemaGap\":false}},\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"informationen_senden\",\"title\":\"Antwort auf „Schicken Sie Informationen“\",\"section\":\"Abschnitt 12\",\"automatic\":false,\"placeholders\":[\"Name\",\"Thema\",\"Link\",\"Entscheidungsfrage\",\"Datum / Zeitraum\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"einwand_dienstleister\",\"title\":\"Antwort auf „Wir haben bereits einen Dienstleister“\",\"section\":\"Abschnitt 13\",\"automatic\":false,\"placeholders\":[\"Name\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"einwand_budget\",\"title\":\"Antwort auf „Aktuell kein Budget“\",\"section\":\"Abschnitt 14\",\"automatic\":false,\"placeholders\":[\"Name\",\"Monat / Quartal\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"einwand_zu_frueh\",\"title\":\"Antwort auf „Noch zu früh / kein konkreter Use Case“\",\"section\":\"Abschnitt 15\",\"automatic\":false,\"placeholders\":[\"Name\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"falsche_ansprechperson\",\"title\":\"Antwort bei falscher Ansprechperson\",\"section\":\"Abschnitt 16\",\"automatic\":false,\"placeholders\":[\"Name\",\"Unternehmen\",\"Zielrolle\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"veranstaltungs_followup\",\"title\":\"Veranstaltungs-Follow-up\",\"section\":\"Abschnitt 17\",\"automatic\":false,\"placeholders\":[\"Name\",\"Veranstaltung\",\"Thema\",\"Entscheidungsfrage\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"},{\"key\":\"kontaktformular_kurzfassung\",\"title\":\"Kontaktformular-Kurzfassung\",\"section\":\"Abschnitt 18\",\"automatic\":false,\"placeholders\":[\"Unternehmen\",\"Triggersignal\",\"Zielrolle\"],\"source\":\"EAIR – Kommunikationsvorlagen für Markteinführung & Outreach, Abschnitte 1–19\"}],\"qualityGates\":[\"Triggersignal, Zielrolle und nächster Schritt müssen belegt oder nachvollziehbar sein.\",\"Platzhalter vollständig ersetzen; ungeprüfte Marktansprache ist gesperrt.\",\"Nur ein primärer Call-to-Action je Nachricht.\",\"Keine garantierten Einsparungen, Ergebnisse, Compliance oder Projekterfolge behaupten.\",\"NOVA ausschließlich als modellierten Demonstrationsfall bezeichnen.\",\"Opt-out, Ablehnung und fehlende Relevanz respektieren und im CRM dokumentieren.\"]};\nconst REQUIRED_FIELDS = [...CRM_REFERENCE.columns, ...CRM_REFERENCE.technicalFields]\n  .filter((field) => field.roles.includes('required'))\n  .map((field) => field.name);\n\nconst sequenceRules = CRM_REFERENCE.states\n  .filter((state) => state.automationRule?.templateKey)\n  .map((state) => ({ status: state.name, ...state.automationRule }));\n\nconst closureRule = CRM_REFERENCE.states.find((state) => state.automationRule?.closesSequence)?.automationRule;\nconst closureEligibleStatuses = new Set([\n  ...sequenceRules.map(({ status }) => status),\n  CRM_REFERENCE.states.find((state) => state.automationRule?.closesSequence)?.name,\n].filter(Boolean));\n\n/**\n * Runtime inputs required to evaluate one lead without external calls.\n *\n * @typedef {object} RunContext\n * @property {string} today ISO date in the workflow timezone.\n * @property {Set<string>} holidays ISO dates that are not business days.\n * @property {object[]} leads All currently loaded leads, used only for duplicate-email detection.\n * @property {{ type: 'email' | 'domain', value: string }[]} optOuts Append-only absolute contact blocks.\n * @property {Set<string>} openDraftLeadIds Lead IDs that already have an unsent draft.\n * @property {object[]} manualSignals Append-only, lifecycle-scoped manual signal events.\n * @property {string} [sequenceEpoch] Explicit append-only epoch of the currently active sequence.\n * @property {string} [sequenceStartedAt] ISO timestamp of the active sequence epoch's append-only start.\n * @property {string} [now] Current ISO timestamp used for temporal signal validation.\n */\n\n/**\n * Deterministic outcome for whether the workflow may prepare a draft.\n *\n * @typedef {object} LeadDecision\n * @property {'create_draft' | 'close_sequence' | 'skip'} action\n * @property {string} reason Machine-readable German explanation for the selected outcome.\n * @property {string | undefined} [templateKey] Canonical template key when a draft is allowed.\n * @property {object | undefined} [statePatch] CRM fields to update when the sequence is closed.\n */\n\n/**\n * Deterministic outcome of comparing a draft event with manually sent messages.\n *\n * @typedef {object} DraftDecision\n * @property {'advance_state' | 'escalate' | 'wait'} action\n * @property {string} reason Machine-readable German explanation for the selected outcome.\n * @property {object | undefined} [statePatch] CRM fields to update after a confirmed manual send.\n */\n\nconst text = (value) => (typeof value === 'string' ? value.trim() : '');\n\nconst eventDetails = (event) => {\n  try { return JSON.parse(event?.details_json || '{}'); } catch { return {}; }\n};\n\n/** Returns the deterministic lifecycle scope for mutations of the lead's current sequence state. */\nfunction sequenceLifecycleId(lead, sequenceEpoch = 'initial') {\n  return ['sequence', text(lead?.['Lead-ID']), 'epoch', text(sequenceEpoch) || 'initial'].join(':');\n}\n\nconst isoTimestamp = (value) => {\n  const iso = text(value);\n  const match = iso.match(/^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,3})?(Z|([+-])(\\d{2}):(\\d{2}))$/u);\n  if (!match) return null;\n  const [, yearText, monthText, dayText, hourText, minuteText, secondText,\n    fraction = '', zone, offsetSign = '+', offsetHourText = '00', offsetMinuteText = '00'] = match;\n  const [year, month, day, hour, minute, second, offsetHour, offsetMinute] = [\n    yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText,\n  ].map(Number);\n  if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59\n    || offsetHour > 23 || offsetMinute > 59) return null;\n  const parsed = Date.parse(iso);\n  if (!Number.isFinite(parsed)) return null;\n  const signedOffsetMinutes = zone === 'Z'\n    ? 0\n    : (offsetSign === '+' ? 1 : -1) * ((offsetHour * 60) + offsetMinute);\n  const localRoundtrip = new Date(parsed + (signedOffsetMinutes * 60_000));\n  const millisecond = Number(fraction.slice(1).padEnd(3, '0') || 0);\n  if (localRoundtrip.getUTCFullYear() !== year || localRoundtrip.getUTCMonth() + 1 !== month\n    || localRoundtrip.getUTCDate() !== day || localRoundtrip.getUTCHours() !== hour\n    || localRoundtrip.getUTCMinutes() !== minute || localRoundtrip.getUTCSeconds() !== second\n    || localRoundtrip.getUTCMilliseconds() !== millisecond) return null;\n  return parsed;\n};\n\nconst hasActiveManualSignal = (manualSignals, {\n  lifecycleId, leadId, sequenceEpoch, notBefore, now,\n}) => {\n  const lowerBound = isoTimestamp(notBefore);\n  const currentTime = isoTimestamp(now);\n  if (currentTime === null || (text(notBefore) && lowerBound === null)) return false;\n  return (manualSignals ?? []).some((signal) => {\n    if (signal?.event_type !== 'manual_signal' || text(signal?.lead_id) !== text(leadId)) return false;\n    const details = eventDetails(signal);\n    if (text(details.sequence_epoch) !== text(sequenceEpoch)\n      || text(details.lifecycle_id) !== lifecycleId) return false;\n    const occurredAt = isoTimestamp(signal?.occurred_at);\n    if (occurredAt === null || (lowerBound !== null && occurredAt < lowerBound)) return false;\n    if (currentTime !== null && occurredAt > currentTime) return false;\n    if (!Object.hasOwn(details, 'expires_at')) return true;\n    const expiresAt = isoTimestamp(details.expires_at);\n    return expiresAt !== null && (currentTime === null || expiresAt >= currentTime);\n  });\n};\n\n/** @param {unknown} value @returns {string} */\nfunction normalizeEmail(value) {\n  return text(value).toLowerCase();\n}\n\n/** @param {unknown} value @returns {string} */\nfunction emailDomain(value) {\n  const email = normalizeEmail(value);\n  const at = email.lastIndexOf('@');\n  return at > 0 && at < email.length - 1 ? email.slice(at + 1) : '';\n}\n\nconst dateAtUtc = (isoDate) => {\n  if (typeof isoDate !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(isoDate)) {\n    throw new TypeError('ISO-Datum erwartet');\n  }\n\n  const [year, month, day] = isoDate.split('-').map(Number);\n  const date = new Date(Date.UTC(year, month - 1, day));\n  if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {\n    throw new RangeError('Ungültiges ISO-Datum');\n  }\n  return date;\n};\n\nconst isoDate = (date) => date.toISOString().slice(0, 10);\n\nconst isBusinessDay = (date, holidays) => {\n  const day = date.getUTCDay();\n  return day !== 0 && day !== 6 && !holidays.has(isoDate(date));\n};\n\n/**\n * Adds weekdays while skipping Saturday, Sunday, and explicitly supplied ISO holidays.\n * @param {string} isoDateValue\n * @param {number} days\n * @param {Set<string>} holidays\n * @returns {string}\n */\nfunction addBusinessDays(isoDateValue, days, holidays) {\n  if (!Number.isInteger(days) || days < 0) throw new RangeError('Nichtnegative ganze Werktage erwartet');\n  if (!(holidays instanceof Set)) throw new TypeError('Feiertage als Set erwartet');\n\n  const date = dateAtUtc(isoDateValue);\n  let remaining = days;\n  while (remaining > 0) {\n    date.setUTCDate(date.getUTCDate() + 1);\n    if (isBusinessDay(date, holidays)) remaining -= 1;\n  }\n  return isoDate(date);\n}\n\nconst businessDaysBetween = (from, to, holidays) => {\n  const date = dateAtUtc(from);\n  const end = dateAtUtc(to);\n  let count = 0;\n  while (date < end) {\n    date.setUTCDate(date.getUTCDate() + 1);\n    if (isBusinessDay(date, holidays)) count += 1;\n  }\n  return count;\n};\n\n/** @param {object} lead @returns {string[]} */\nfunction validateRequiredFields(lead) {\n  return REQUIRED_FIELDS.filter((field) => !text(lead?.[field]));\n}\n\nconst renderTemplateText = (sourceText, mappings, lead, missing) => sourceText.replace(/\\[([^\\]]+)\\]/gu, (_, token) => {\n  const mapping = mappings[token];\n  if (!mapping) throw new TypeError(`Nicht dokumentierter Vorlagenplatzhalter: ${token}`);\n  const value = mapping.fields.map((field) => text(lead?.[field])).find(Boolean)\n    || text(lead?.[mapping.missingLabel]);\n  if (value) return value;\n  if (!missing.includes(mapping.missingLabel)) missing.push(mapping.missingLabel);\n  return `[[FEHLT: ${mapping.missingLabel}]]`;\n});\n\n/**\n * Renders one canonical template without inventing facts: missing placeholder values remain visible.\n * @param {string} templateKey\n * @param {object} lead\n * @returns {{ subject: string, body: string, missing: string[] }}\n */\nfunction renderTemplate(templateKey, lead) {\n  const template = CRM_REFERENCE.templates.find((entry) => entry.key === templateKey);\n  if (!template) throw new RangeError(`Unbekannte Vorlage: ${templateKey}`);\n  if (!template.automatic || !template.subjectTemplate || !template.bodyTemplate || !template.placeholderMappings) {\n    throw new RangeError(`Vorlage ist nicht für automatische Entwürfe freigegeben: ${templateKey}`);\n  }\n\n  const missing = [];\n  return {\n    subject: renderTemplateText(template.subjectTemplate, template.placeholderMappings, lead, missing),\n    body: renderTemplateText(template.bodyTemplate, template.placeholderMappings, lead, missing),\n    missing,\n  };\n}\n\nconst isOptedOut = (lead, optOuts) => {\n  const localOptOut = text(lead?.['Opt-out / keine weitere Ansprache']).toLowerCase();\n  if (localOptOut === 'ja' || localOptOut === 'true') return true;\n\n  const email = normalizeEmail(lead?.['E-Mail']);\n  const domain = emailDomain(email);\n  return (optOuts ?? []).some((entry) => (\n    (entry.type === 'email' && normalizeEmail(entry.value) === email)\n    || (entry.type === 'domain' && text(entry.value).toLowerCase() === domain)\n  ));\n};\n\nconst isDuplicate = (lead, leads) => {\n  const email = normalizeEmail(lead?.['E-Mail']);\n  if (!email) return false;\n  const domain = emailDomain(email);\n  const validDomain = domain.length <= 253 && domain.includes('.')\n    && domain.split('.').every((label) => (\n      label.length > 0 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)\n    ));\n  const leadId = text(lead?.['Lead-ID']);\n  return (leads ?? []).some((candidate) => {\n    if (candidate === lead) return false;\n    const candidateLeadId = text(candidate?.['Lead-ID']);\n    if (leadId && candidateLeadId === leadId) return false;\n    const candidateEmail = normalizeEmail(candidate?.['E-Mail']);\n    if (!candidateEmail) return false;\n    if (candidateEmail === email) return true;\n    return validDomain && emailDomain(candidateEmail) === domain;\n  });\n};\n\nconst isDue = (lead, today) => {\n  const dueDate = text(lead?.Wiedervorlage);\n  return Boolean(dueDate) && dueDate <= today;\n};\n\n/**\n * Chooses at most one draft action after applying all CRM gates in their required order.\n * @param {object} lead\n * @param {RunContext} context\n * @returns {LeadDecision}\n */\nfunction planLeadAction(lead, context) {\n  if (validateRequiredFields(lead).length > 0) return { action: 'skip', reason: 'Pflichtfeld-Lücke' };\n  if (isDuplicate(lead, context?.leads)) return { action: 'skip', reason: 'Dublette' };\n  if (isOptedOut(lead, context?.optOuts)) return { action: 'skip', reason: 'Opt-out' };\n\n  const attempt = Number(lead?.Versuch);\n  const sequenceEpoch = text(context?.sequenceEpoch) || 'initial';\n  if (hasActiveManualSignal(context?.manualSignals, {\n    lifecycleId: sequenceLifecycleId(lead, sequenceEpoch),\n    leadId: lead?.['Lead-ID'],\n    sequenceEpoch,\n    notBefore: context?.sequenceStartedAt,\n    now: context?.now || `${context?.today ?? ''}T12:00:00Z`,\n  })) {\n    return { action: 'skip', reason: 'Neues manuelles Signal' };\n  }\n  if (closureRule && attempt >= closureRule.attempt && closureEligibleStatuses.has(text(lead?.Kontaktstatus))) {\n    if (!isDue(lead, context?.today ?? '')) return { action: 'skip', reason: 'Nicht fällig' };\n    return {\n      action: 'close_sequence',\n      reason: 'Versuchslimit',\n      statePatch: {\n        Kontaktstatus: closureRule.nextState,\n        Wiedervorlage: '',\n        'Einwände / Erkenntnisse': 'Keine Reaktion nach drei Versuchen; neues manuelles Signal erforderlich',\n      },\n    };\n  }\n  if (context?.openDraftLeadIds?.has(text(lead?.['Lead-ID']))) return { action: 'skip', reason: 'Offener Entwurf' };\n\n  const rule = sequenceRules.find((entry) => entry.status === lead?.Kontaktstatus && entry.attempt === attempt);\n  if (!rule) return { action: 'skip', reason: 'Zustandsregel' };\n  if (!isDue(lead, context?.today ?? '')) return { action: 'skip', reason: 'Nicht fällig' };\n\n  return { action: 'create_draft', reason: 'Fällig', templateKey: rule.templateKey };\n}\n\nconst matchesSentMessage = (event, message) => {\n  const messageId = text(event?.gmail_message_id);\n  if (messageId) {\n    return [message?.id, message?.messageId, message?.gmail_message_id]\n      .some((value) => text(value) === messageId);\n  }\n\n  const draftId = text(event?.gmail_draft_id);\n  return Boolean(draftId && [message?.draftId, message?.draft_id, message?.gmail_draft_id]\n    .some((value) => text(value) === draftId));\n};\n\n/**\n * Reconciles one draft event only after a matching manual-send record is observed.\n * @param {object} event\n * @param {object[]} sentMessages\n * @param {string} today ISO date in the workflow timezone.\n * @param {Set<string>} holidays\n * @param {object} liveLead freshly loaded lead row used to guard manual signals and state drift.\n * @param {{ type: 'email' | 'domain', value: string }[]} [optOuts]\n * @param {object[]} [manualSignals] append-only lifecycle-scoped manual signal events.\n * @param {string} [now] retained for backwards-compatible workflow invocation.\n * @returns {DraftDecision}\n */\nfunction reconcileDraft(\n  event, sentMessages, today, holidays, liveLead, optOuts = [], manualSignals = [],\n  now = `${today}T12:00:00Z`,\n) {\n  const wasSent = (sentMessages ?? []).some((message) => matchesSentMessage(event, message));\n  if (wasSent) {\n    const templateKey = text(event?.template_key || event?.templateKey);\n    const rule = sequenceRules.find((entry) => entry.templateKey === templateKey);\n    if (!rule) return { action: 'wait', reason: 'Unbekannte Vorlage' };\n\n    const lifecycleId = text(eventDetails(event).lifecycle_id)\n      || text(event?.event_id).replace(/:(?:reserved|created)$/u, '');\n    const sequenceEpoch = text(eventDetails(event).sequence_epoch) || 'initial';\n    if (hasActiveManualSignal(manualSignals, {\n      lifecycleId,\n      leadId: event?.lead_id || liveLead?.['Lead-ID'],\n      sequenceEpoch,\n      notBefore: event?.occurred_at || event?.created_at,\n      now,\n    })) return { action: 'wait', reason: 'Neues manuelles Signal schützt Lebenszyklus' };\n\n    const stateIsStillEligible = text(liveLead?.Kontaktstatus) === rule.status\n      && Number(liveLead?.Versuch) === rule.attempt\n      && !isOptedOut(liveLead, optOuts);\n    if (!stateIsStillEligible) return { action: 'wait', reason: 'Manuelles Signal schützt Zustand' };\n\n    const statePatch = {\n      Kontaktstatus: rule.nextState,\n      Versuch: rule.nextAttempt,\n      'Letzter Kontakt': today,\n    };\n    if (rule.waitBusinessDays !== null) statePatch.Wiedervorlage = addBusinessDays(today, rule.waitBusinessDays, holidays);\n    return { action: 'advance_state', reason: 'Manuell versendet', statePatch };\n  }\n\n  const occurredAt = text(event?.occurred_at || event?.created_at);\n  if (isoTimestamp(occurredAt) === null) return { action: 'wait', reason: 'Entwurf offen' };\n  const occurredOn = occurredAt.slice(0, 10);\n  if (occurredOn && businessDaysBetween(occurredOn, today, holidays) >= 4) {\n    return { action: 'escalate', reason: 'Entwurf liegt' };\n  }\n  return { action: 'wait', reason: 'Entwurf offen' };\n}\n\n\nconst leads = $('Sheets: Leads laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst optOuts = $('Sheets: Opt-outs laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst rawEvents = $('Sheets: Events laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst trackerRows = $('Sheets: Validierungstracker laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst openDrafts = $('Gmail: Offene Entwürfe laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst sentMessages = $('Gmail: Gesendete Nachrichten laden').all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst holidays = new Set($('Sheets: Feiertage laden').all().map((item) => item.json.datum).filter(Boolean));\nconst run = $('Code: Run-Kontext').first().json;\nconst today = $now.setZone('Europe/Berlin').toISODate();\nconst now = $now.setZone('Europe/Berlin').toISO();\nconst eventKey = (event, index) => String(event.event_id || ['legacy', event.lead_id, event.event_type, event.occurred_at, index].join(':'));\nconst uniqueEvents = [];\nconst seenEventKeys = new Set();\nfor (const [index, event] of rawEvents.entries()) {\n  const key = eventKey(event, index);\n  if (seenEventKeys.has(key)) continue;\n  seenEventKeys.add(key);\n  uniqueEvents.push(event);\n}\nconst details = (event) => {\n  try { return JSON.parse(event.details_json || '{}'); } catch { return {}; }\n};\nconst nowTimestamp = isoTimestamp(now);\nconst lifecycleId = (event) => {\n  const explicit = details(event).lifecycle_id;\n  if (explicit) return String(explicit);\n  const eventId = String(event.event_id || '');\n  const suffix = eventId.match(/:(?:reserved|created|advance_state|escalate|recovery_pending|recovery_conflict|cancelled)$/u);\n  return suffix ? eventId.slice(0, -suffix[0].length) : ['legacy-draft', event.lead_id, event.gmail_draft_id || event.gmail_message_id || event.occurred_at].join(':');\n};\nconst sequenceEpochFromEvent = (event) => {\n  const explicit = String(details(event).sequence_epoch || '');\n  if (explicit) return explicit;\n  return lifecycleId(event).match(/:epoch:([^:]+)(?::|$)/u)?.[1] || 'initial';\n};\nconst validEpochToken = (value) => /^[A-Za-z0-9._-]+$/u.test(String(value || ''));\nconst sequenceEpochByLead = new Map();\nconst sequenceStartedAtByLead = new Map();\nconst firstReopenByLeadEpoch = new Map();\nfor (const event of uniqueEvents\n  .filter((candidate) => candidate.event_type === 'sequence_reopened'\n    && isoTimestamp(candidate.occurred_at) !== null\n    && isoTimestamp(candidate.occurred_at) <= nowTimestamp)\n  .sort((left, right) => isoTimestamp(left.occurred_at) - isoTimestamp(right.occurred_at))) {\n  const leadId = String(event.lead_id || '');\n  const epoch = String(details(event).sequence_epoch || '');\n  const sequenceLifecycle = ['sequence', leadId, 'epoch', epoch].join(':');\n  const leadEpochKey = leadId + '|' + epoch;\n  if (!leadId || epoch === 'initial' || !validEpochToken(epoch)\n    || String(event.event_id || '') !== sequenceLifecycle + ':reopened'\n    || String(details(event).lifecycle_id || '') !== sequenceLifecycle\n    || firstReopenByLeadEpoch.has(leadEpochKey)) continue;\n  firstReopenByLeadEpoch.set(leadEpochKey, event);\n  sequenceEpochByLead.set(leadId, epoch);\n  sequenceStartedAtByLead.set(leadId, String(event.occurred_at || ''));\n}\nconst activeEpoch = (leadId) => sequenceEpochByLead.get(String(leadId || '')) || 'initial';\nconst sequenceStartedAt = (leadId) => sequenceStartedAtByLead.get(String(leadId || '')) || '';\nconst advancedLifecycles = new Set();\nconst eventIds = new Set(uniqueEvents.map((event) => String(event.event_id || '')).filter(Boolean));\nconst actions = [];\nconst openDraftLeadIds = new Set();\nconst reportCategories = { 'Opt-out': 'opt_out', Dublette: 'duplicate', 'Pflichtfeld-Lücke': 'required_fields' };\nconst manualSignals = uniqueEvents\n  .filter((event) => (event.event_type === 'manual_signal' || details(event).new_manual_signal === true)\n    && details(event).boundary !== 'validation_tracker');\nconst gmailSubject = (message) => {\n  const headers = message.message?.payload?.headers || message.payload?.headers || message.headers || [];\n  const subjectHeader = Array.isArray(headers)\n    ? headers.find((header) => String(header.name || '').toLowerCase() === 'subject')?.value\n    : headers.Subject || headers.subject;\n  return String(message.subject || message.Subject || message.message?.subject || subjectHeader || '');\n};\nconst gmailIdentity = (message) => ({\n  gmail_draft_id: message.draftId || message.draft_id || '',\n  gmail_message_id: message.messageId || message.message?.id || message.id || '',\n  gmail_thread_id: message.threadId || message.message?.threadId || '',\n});\n\nconst reservationByLifecycle = new Map();\nconst createdByLifecycle = new Map();\nfor (const event of uniqueEvents) {\n  const lifecycle = lifecycleId(event);\n  if (event.event_type === \"draft_reserved\" && !reservationByLifecycle.has(lifecycle)) {\n    reservationByLifecycle.set(lifecycle, event);\n  }\n  if (event.event_type === \"draft_created\" && !createdByLifecycle.has(lifecycle)) {\n    createdByLifecycle.set(lifecycle, event);\n  }\n}\nconst lifecycleRecords = [...new Set([...reservationByLifecycle.keys(), ...createdByLifecycle.keys()])]\n  .map((lifecycle) => {\n    const reservation = reservationByLifecycle.get(lifecycle);\n    const created = createdByLifecycle.get(lifecycle);\n    const source = created || reservation;\n    return {\n      lifecycle, reservation, created, source,\n      leadId: String(source?.lead_id || ''), sequenceEpoch: sequenceEpochFromEvent(source),\n      marker: String(source?.subject_marker || reservation?.subject_marker || ''),\n      occurredAt: String(reservation?.occurred_at || created?.occurred_at || ''),\n    };\n  })\n  .sort((left, right) => [left.occurredAt, left.lifecycle].join('|').localeCompare([right.occurredAt, right.lifecycle].join('|')));\nconst recordByLifecycle = new Map(lifecycleRecords.map((record) => [record.lifecycle, record]));\nconst validPendingFor = (event, record) => {\n  const reservationTime = isoTimestamp(record.reservation?.occurred_at);\n  const pendingTime = isoTimestamp(event?.occurred_at);\n  return event?.event_type === 'recovery_pending'\n    && String(event.event_id || '') === record.lifecycle + ':recovery_pending'\n    && String(event.lead_id || '') === record.leadId\n    && String(details(event).lifecycle_id || '') === record.lifecycle\n    && String(details(event).sequence_epoch || '') === record.sequenceEpoch\n    && String(details(event).cancellation_event_id || '') === record.lifecycle + ':cancelled'\n    && String(details(event).reservation_event_id || '') === String(record.reservation?.event_id || '')\n    && reservationTime !== null && pendingTime !== null && nowTimestamp !== null\n    && reservationTime < pendingTime && pendingTime <= nowTimestamp;\n};\nconst pendingFor = (record) => uniqueEvents.find((event) => validPendingFor(event, record));\nconst messageMatchesIdentity = (event, message) => {\n  const messageId = String(event?.gmail_message_id || '');\n  const draftId = String(event?.gmail_draft_id || '');\n  return Boolean(\n    (messageId && [message?.id, message?.messageId, message?.gmail_message_id, message?.message?.id]\n      .some((value) => String(value || '') === messageId))\n    || (draftId && [message?.draftId, message?.draft_id, message?.gmail_draft_id]\n      .some((value) => String(value || '') === draftId))\n  );\n};\nconst sentEvidence = (record) => sentMessages.find((message) => (\n  messageMatchesIdentity(record.source, message)\n  || (record.marker && gmailSubject(message).includes(record.marker))\n));\nconst draftEvidence = (record) => openDrafts.find((message) => (\n  messageMatchesIdentity(record.source, message)\n  || (record.marker && gmailSubject(message).includes(record.marker))\n));\nconst sourceEventById = (eventId) => uniqueEvents.find((event) => (\n  String(event.event_id || '') === String(eventId || '')\n));\nconst validDraftSourceFor = (source, record) => {\n  const sourceDetails = details(source);\n  const sourceSuffix = source?.event_type === \"draft_created\" ? ':created'\n    : source?.event_type === \"draft_reserved\" ? ':reserved' : '';\n  return Boolean(source && sourceSuffix && record?.leadId && record.lifecycle && record.marker)\n    && String(source.event_id || '') === record.lifecycle + sourceSuffix\n    && String(source.lead_id || '') === record.leadId\n    && String(sourceDetails.lifecycle_id || '') === record.lifecycle\n    && String(sourceDetails.sequence_epoch || '') === record.sequenceEpoch\n    && String(source.subject_marker || '') === record.marker;\n};\nconst validSentRecoverySourceFor = (source, record, sentMessageId, notAfterTime = nowTimestamp) => {\n  const sourceDetails = details(source);\n  const reservationTime = isoTimestamp(record?.reservation?.occurred_at);\n  const sourceTime = isoTimestamp(source?.occurred_at);\n  return validDraftSourceFor(source, record)\n    && validDraftSourceFor(record?.reservation, record)\n    && source.event_type === \"draft_created\"\n    && sourceDetails.recovered_from_subject_marker === true\n    && String(sourceDetails.recovered_location || '') === 'sent'\n    && Boolean(sentMessageId)\n    && String(source.gmail_message_id || '') === sentMessageId\n    && reservationTime !== null && sourceTime !== null && nowTimestamp !== null && notAfterTime !== null\n    && reservationTime < sourceTime && sourceTime <= notAfterTime && sourceTime <= nowTimestamp;\n};\nconst validAdvanceSentEvidenceFor = (event, record, source, sentMessageId, conflictTime = null) => {\n  const advanceDetails = details(event);\n  const reservationTime = isoTimestamp(record?.reservation?.occurred_at);\n  const sourceTime = isoTimestamp(source?.occurred_at);\n  const advanceTime = isoTimestamp(event?.occurred_at);\n  return Boolean(event && record && source)\n    && validDraftSourceFor(source, record)\n    && event.event_type === \"advance_state\"\n    && String(event.event_id || '') === record.lifecycle + ':advance_state'\n    && String(event.lead_id || '') === record.leadId\n    && String(advanceDetails.lifecycle_id || '') === record.lifecycle\n    && String(advanceDetails.sequence_epoch || '') === record.sequenceEpoch\n    && String(event.subject_marker || '') === record.marker\n    && String(event.gmail_draft_id || '') === String(source.gmail_draft_id || '')\n    && String(event.gmail_message_id || '') === String(source.gmail_message_id || '')\n    && String(event.gmail_thread_id || '') === String(source.gmail_thread_id || '')\n    && Boolean(sentMessageId)\n    && String(event.gmail_message_id || '') === sentMessageId\n    && sourceTime !== null && advanceTime !== null && nowTimestamp !== null\n    && (!record.reservation || (validDraftSourceFor(record.reservation, record)\n      && reservationTime !== null && reservationTime < sourceTime))\n    && sourceTime < advanceTime && advanceTime <= nowTimestamp\n    && (conflictTime === null || (sourceTime < conflictTime && conflictTime < advanceTime));\n};\nconst durableSentEvidenceFor = (record) => {\n  const source = record?.source;\n  const sourceMessageId = String(source?.gmail_message_id || '');\n  if (validSentRecoverySourceFor(source, record, sourceMessageId)) return {\n    type: 'sent_recovery', eventId: String(source.event_id || ''), messageId: sourceMessageId,\n  };\n  const advance = sourceEventById(record?.lifecycle + ':advance_state');\n  const advanceMessageId = String(advance?.gmail_message_id || '');\n  if (validAdvanceSentEvidenceFor(advance, record, source, advanceMessageId)) return {\n    type: 'advance_state', eventId: String(advance.event_id || ''), messageId: advanceMessageId,\n  };\n  return null;\n};\nfor (const record of lifecycleRecords) {\n  const advance = sourceEventById(record.lifecycle + ':advance_state');\n  const advanceMessageId = String(advance?.gmail_message_id || '');\n  if (validAdvanceSentEvidenceFor(advance, record, record.source, advanceMessageId)) {\n    advancedLifecycles.add(record.lifecycle);\n  }\n}\nconst validConflictFor = (event, record) => {\n  const conflictDetails = details(event);\n  const authoritativeLifecycle = String(conflictDetails.authoritative_lifecycle_id || '');\n  const authoritative = recordByLifecycle.get(authoritativeLifecycle);\n  const authoritativeSource = sourceEventById(conflictDetails.authoritative_source_event_id);\n  const conflictingSource = sourceEventById(conflictDetails.conflicting_source_event_id);\n  const authoritativeSentMessageId = String(conflictDetails.authoritative_sent_message_id || '');\n  const sentEvidenceType = String(conflictDetails.authoritative_sent_evidence_type || '');\n  const sentEvidenceEventId = String(conflictDetails.authoritative_sent_evidence_event_id || '');\n  const authoritativeLiveSent = authoritative && sentEvidence(authoritative);\n  const authoritativeLiveSentMessageId = authoritativeLiveSent\n    ? gmailIdentity(authoritativeLiveSent).gmail_message_id : '';\n  const sourceTime = isoTimestamp(record.occurredAt);\n  const authoritativeReservationTime = isoTimestamp(authoritative?.reservation?.occurred_at);\n  const authoritativeSourceTime = isoTimestamp(authoritativeSource?.occurred_at);\n  const conflictTime = isoTimestamp(event?.occurred_at);\n  const validSentRecoveryEvidence = sentEvidenceType === 'sent_recovery'\n    && sentEvidenceEventId === String(authoritativeSource?.event_id || '')\n    && validSentRecoverySourceFor(\n      authoritativeSource, authoritative, authoritativeSentMessageId, conflictTime,\n    );\n  const validAdvanceEvidenceDeclaration = sentEvidenceType === 'advance_state'\n    && sentEvidenceEventId === authoritativeLifecycle + ':advance_state';\n  const validPersistedAdvanceEvidence = validAdvanceEvidenceDeclaration\n    && validAdvanceSentEvidenceFor(\n      sourceEventById(sentEvidenceEventId), authoritative, authoritativeSource,\n      authoritativeSentMessageId, conflictTime,\n    );\n  const validLiveSentEvidence = validAdvanceEvidenceDeclaration\n    && Boolean(authoritativeLiveSentMessageId)\n    && authoritativeLiveSentMessageId === authoritativeSentMessageId;\n  return event?.event_type === 'recovery_conflict'\n    && String(event.event_id || '') === record.lifecycle + ':recovery_conflict'\n    && String(event.lead_id || '') === record.leadId\n    && String(conflictDetails.lifecycle_id || '') === record.lifecycle\n    && String(conflictDetails.sequence_epoch || '') === record.sequenceEpoch\n    && validDraftSourceFor(conflictingSource, record)\n    && validDraftSourceFor(authoritativeSource, authoritative)\n    && authoritativeLifecycle !== record.lifecycle\n    && String(conflictDetails.authoritative_lead_id || '') === authoritative.leadId\n    && authoritative.leadId === record.leadId\n    && String(conflictDetails.authoritative_sequence_epoch || '') === authoritative.sequenceEpoch\n    && String(conflictDetails.authoritative_subject_marker || '') === authoritative.marker\n    && Boolean(authoritativeSentMessageId)\n    && String(authoritativeSource.gmail_message_id || '') === authoritativeSentMessageId\n    && (validSentRecoveryEvidence || validPersistedAdvanceEvidence || validLiveSentEvidence)\n    && String(conflictDetails.conflicting_lifecycle_id || '') === record.lifecycle\n    && String(conflictDetails.conflicting_lead_id || '') === record.leadId\n    && String(conflictDetails.conflicting_sequence_epoch || '') === record.sequenceEpoch\n    && String(conflictDetails.conflicting_subject_marker || '') === record.marker\n    && String(event.subject_marker || '') === record.marker\n    && String(event.gmail_draft_id || '') === String(conflictingSource.gmail_draft_id || '')\n    && String(event.gmail_message_id || '') === String(conflictingSource.gmail_message_id || '')\n    && String(event.gmail_thread_id || '') === String(conflictingSource.gmail_thread_id || '')\n    && String(conflictDetails.reason || '') === 'Paralleler Draft nach später SENT-Evidenz'\n    && String(conflictDetails.cancellation_event_id || '') === record.lifecycle + ':cancelled'\n    && String(conflictDetails.cleanup_instruction || '') === 'Konfliktentwurf in Gmail manuell prüfen und löschen oder eindeutig abschließen'\n    && sourceTime !== null && authoritativeSourceTime !== null\n    && (!authoritative.reservation || (validDraftSourceFor(authoritative.reservation, authoritative)\n      && authoritativeReservationTime !== null && authoritativeReservationTime < authoritativeSourceTime))\n    && conflictTime !== null && nowTimestamp !== null\n    && authoritativeSourceTime < conflictTime\n    && sourceTime < conflictTime && conflictTime <= nowTimestamp;\n};\nconst conflictFor = (record) => rawEvents.find((event) => validConflictFor(event, record));\nconst validCancellationFor = (event, record) => {\n  const cancellationDetails = details(event);\n  const cancellationTime = isoTimestamp(event?.occurred_at);\n  const pending = pendingFor(record);\n  const conflict = conflictFor(record);\n  const lineage = conflict || pending;\n  const lineageTime = isoTimestamp(lineage?.occurred_at);\n  const cancellationEventId = String(details(lineage).cancellation_event_id || '');\n  return event?.event_type === 'cancelled'\n    && cancellationEventId === record.lifecycle + ':cancelled'\n    && String(event.event_id || '') === cancellationEventId\n    && String(event.lead_id || '') === record.leadId\n    && String(cancellationDetails.lifecycle_id || '') === record.lifecycle\n    && String(cancellationDetails.sequence_epoch || '') === record.sequenceEpoch\n    && cancellationTime !== null && lineageTime !== null && nowTimestamp !== null\n    && lineageTime < cancellationTime && cancellationTime <= nowTimestamp;\n};\nconst cancellationFor = (record) => uniqueEvents.find((event) => validCancellationFor(event, record));\nconst authoritativeSentByLead = new Map();\nconst authoritativeSentEvidenceByLifecycle = new Map();\nfor (const record of lifecycleRecords) {\n  const liveSent = sentEvidence(record);\n  const liveMessageId = liveSent ? gmailIdentity(liveSent).gmail_message_id : '';\n  const authoritativeEvidence = durableSentEvidenceFor(record) || (liveMessageId ? {\n    type: 'advance_state', eventId: record.lifecycle + ':advance_state', messageId: liveMessageId,\n  } : null);\n  if (!authoritativeEvidence) continue;\n  authoritativeSentEvidenceByLifecycle.set(record.lifecycle, authoritativeEvidence);\n  const current = authoritativeSentByLead.get(record.leadId);\n  if (!current || (advancedLifecycles.has(current.lifecycle) && !advancedLifecycles.has(record.lifecycle))) {\n    authoritativeSentByLead.set(record.leadId, record);\n  }\n}\nconst isUnresolved = (record) => {\n  if (advancedLifecycles.has(record.lifecycle)) return false;\n  if (sentEvidence(record) || draftEvidence(record)) return true;\n  if (cancellationFor(record)) return false;\n  return Boolean(record.created || record.reservation);\n};\nconst recoveredSourceByLifecycle = new Map();\nfor (const record of lifecycleRecords) {\n  if (!isUnresolved(record)) continue;\n  openDraftLeadIds.add(record.leadId);\n  const sent = sentEvidence(record);\n  const draft = draftEvidence(record);\n  if (!record.created && (sent || draft)) {\n    const recoveredMessage = sent || draft;\n    const recoveredSourceEvent = {\n      ...record.reservation, event_id: record.lifecycle + ':created',\n      event_type: \"draft_created\", ...gmailIdentity(recoveredMessage),\n      occurred_at: now,\n      details_json: JSON.stringify({\n        lifecycle_id: record.lifecycle, sequence_epoch: record.sequenceEpoch,\n        recovered_from_subject_marker: true, recovered_location: sent ? 'sent' : 'draft',\n      }),\n    };\n    recoveredSourceByLifecycle.set(record.lifecycle, recoveredSourceEvent);\n    const actionId = record.lifecycle + ':created';\n    if (!eventIds.has(actionId)) actions.push({\n      kind: sent ? 'writeback' : 'label_recovery',\n      actionType: \"draft_created\", resultType: 'draft_recovered', actionId,\n      lifecycleId: record.lifecycle, runId: run.runId, reason: 'Verwaisten Entwurf wiederhergestellt',\n      sourceEvent: recoveredSourceEvent, eventDetails: {\n        sequence_epoch: record.sequenceEpoch, recovered_from_subject_marker: true,\n        recovered_location: sent ? 'sent' : 'draft',\n      },\n      recovery: { location: sent ? 'sent' : 'draft', subjectMarker: record.marker },\n    });\n  } else if (!record.created && !sent && !draft) {\n    const pendingActionId = record.lifecycle + ':recovery_pending';\n    actions.push({\n      kind: eventIds.has(pendingActionId) ? 'noop' : 'writeback', actionType: 'recovery_pending',\n      actionId: pendingActionId, lifecycleId: record.lifecycle, runId: run.runId,\n      reason: 'Reservierung ohne Gmail-Treffer', sourceEvent: record.reservation,\n      eventDetails: {\n        sequence_epoch: record.sequenceEpoch, cancellation_event_id: record.lifecycle + ':cancelled',\n        reservation_event_id: record.reservation.event_id, subject_marker: record.marker,\n      },\n      diagnostic: { category: 'recovery_pending', cancellationEventId: record.lifecycle + ':cancelled' },\n    });\n  }\n}\nconst conflictLineageBase = (lifecycle) => String(lifecycle || '').replace(/:retry:\\d+$/u, '');\nfor (const [leadId, authoritative] of authoritativeSentByLead) {\n  for (const conflicting of lifecycleRecords.filter((record) => (\n    record.leadId === leadId\n    && record.lifecycle !== authoritative.lifecycle\n    && conflictLineageBase(record.lifecycle) === conflictLineageBase(authoritative.lifecycle)\n    && isUnresolved(record)\n  ))) {\n    const actionId = conflicting.lifecycle + ':recovery_conflict';\n    const cancellationEventId = conflicting.lifecycle + ':cancelled';\n    const authoritativeEvidence = authoritativeSentEvidenceByLifecycle.get(authoritative.lifecycle);\n    const authoritativeSource = recoveredSourceByLifecycle.get(authoritative.lifecycle) || authoritative.source;\n    const conflictingSource = recoveredSourceByLifecycle.get(conflicting.lifecycle) || conflicting.source;\n    const authoritativeSentMessageId = String(authoritativeEvidence?.messageId || '');\n    const authoritativeHasSentRecovery = validSentRecoverySourceFor(\n      authoritativeSource, authoritative, authoritativeSentMessageId,\n    );\n    actions.push({\n      kind: conflictFor(conflicting) ? 'noop' : 'writeback', actionType: 'recovery_conflict', actionId,\n      lifecycleId: conflicting.lifecycle, runId: run.runId,\n      reason: 'Paralleler Draft nach später SENT-Evidenz', sourceEvent: conflictingSource,\n      eventDetails: {\n        sequence_epoch: conflicting.sequenceEpoch,\n        authoritative_lifecycle_id: authoritative.lifecycle,\n        authoritative_source_event_id: authoritativeSource.event_id,\n        authoritative_lead_id: authoritative.leadId,\n        authoritative_sequence_epoch: authoritative.sequenceEpoch,\n        authoritative_sent_message_id: authoritativeSentMessageId,\n        authoritative_sent_evidence_type: authoritativeHasSentRecovery\n          ? 'sent_recovery' : String(authoritativeEvidence?.type || 'advance_state'),\n        authoritative_sent_evidence_event_id: authoritativeHasSentRecovery\n          ? authoritativeSource.event_id : String(authoritativeEvidence?.eventId || authoritative.lifecycle + ':advance_state'),\n        conflicting_lifecycle_id: conflicting.lifecycle,\n        conflicting_source_event_id: conflictingSource.event_id,\n        conflicting_lead_id: conflicting.leadId,\n        conflicting_sequence_epoch: conflicting.sequenceEpoch,\n        authoritative_subject_marker: authoritative.marker,\n        conflicting_subject_marker: conflicting.marker,\n        cancellation_event_id: cancellationEventId,\n        cleanup_instruction: 'Konfliktentwurf in Gmail manuell prüfen und löschen oder eindeutig abschließen',\n      },\n      diagnostic: {\n        category: 'recovery_conflict', authoritativeLifecycleId: authoritative.lifecycle,\n        conflictingLifecycleId: conflicting.lifecycle, authoritativeSubjectMarker: authoritative.marker,\n        conflictingSubjectMarker: conflicting.marker, cancellationEventId,\n        cleanupInstruction: 'Konfliktentwurf in Gmail manuell prüfen und löschen oder eindeutig abschließen',\n      },\n    });\n  }\n}\nfor (const event of rawEvents.filter((candidate) => candidate.event_type === 'recovery_conflict')) {\n  const conflictLifecycle = lifecycleId(event);\n  const conflictRecord = recordByLifecycle.get(conflictLifecycle);\n  const conflictDetails = details(event);\n  const actionId = conflictLifecycle + ':recovery_conflict';\n  if (!conflictRecord || !isUnresolved(conflictRecord) || !validConflictFor(event, conflictRecord)\n    || actions.some((action) => action.actionId === actionId)) continue;\n  actions.push({\n    kind: 'noop', actionType: 'recovery_conflict', actionId,\n    lifecycleId: conflictLifecycle, runId: run.runId,\n    reason: String(conflictDetails.reason || 'Paralleler Draft nach später SENT-Evidenz'),\n    sourceEvent: conflictRecord.source,\n    diagnostic: {\n      category: 'recovery_conflict',\n      authoritativeLifecycleId: String(conflictDetails.authoritative_lifecycle_id || ''),\n      conflictingLifecycleId: conflictLifecycle,\n      authoritativeSubjectMarker: String(conflictDetails.authoritative_subject_marker || ''),\n      conflictingSubjectMarker: String(conflictDetails.conflicting_subject_marker || conflictRecord.marker),\n      cancellationEventId: String(conflictDetails.cancellation_event_id || ''),\n      cleanupInstruction: String(conflictDetails.cleanup_instruction || 'Konflikt manuell prüfen'),\n    },\n  });\n}\nconst leadAdvancePlanned = new Set();\nfor (const [leadId, record] of authoritativeSentByLead) {\n  if (advancedLifecycles.has(record.lifecycle) || leadAdvancePlanned.has(leadId)) continue;\n  const lead = leads.find((candidate) => String(candidate['Lead-ID'] || '') === leadId);\n  const sourceEvent = recoveredSourceByLifecycle.get(record.lifecycle) || record.created || record.reservation;\n  const authoritativeEvidence = authoritativeSentEvidenceByLifecycle.get(record.lifecycle);\n  const reconciliationMessages = sentEvidence(record) ? sentMessages : [{ id: authoritativeEvidence?.messageId || '' }];\n  const reconciliation = reconcileDraft(sourceEvent, reconciliationMessages, today, holidays, lead, optOuts, manualSignals, now);\n  const actionId = record.lifecycle + ':advance_state';\n  if (reconciliation.action === 'advance_state' && !eventIds.has(actionId)) {\n    actions.push({\n      kind: 'writeback', actionType: reconciliation.action, actionId,\n      lifecycleId: record.lifecycle, lead, statePatch: reconciliation.statePatch,\n      reason: record.created ? reconciliation.reason : 'Gesendeten verwaisten Entwurf wiederhergestellt',\n      sourceEvent, eventDetails: { sequence_epoch: record.sequenceEpoch }, runId: run.runId,\n      recovery: { location: 'sent', subjectMarker: record.marker },\n    });\n    leadAdvancePlanned.add(leadId);\n  }\n}\nfor (const record of lifecycleRecords) {\n  if (!record.created || advancedLifecycles.has(record.lifecycle) || sentEvidence(record)\n    || cancellationFor(record) || conflictFor(record)\n    || actions.some((action) => action.actionId === record.lifecycle + ':recovery_conflict')\n    || authoritativeSentByLead.get(record.leadId)?.lifecycle === record.lifecycle) continue;\n  const lead = leads.find((candidate) => String(candidate['Lead-ID'] || '') === record.leadId);\n  const reconciliation = reconcileDraft(record.created, sentMessages, today, holidays, lead, optOuts, manualSignals, now);\n  if (reconciliation.action === 'wait') continue;\n  const actionId = record.lifecycle + ':' + reconciliation.action;\n  if (!eventIds.has(actionId)) actions.push({\n    kind: 'writeback', actionType: reconciliation.action, actionId,\n    lifecycleId: record.lifecycle, lead, statePatch: reconciliation.statePatch,\n    reason: reconciliation.reason, sourceEvent: record.created,\n    eventDetails: { sequence_epoch: record.sequenceEpoch }, runId: run.runId,\n  });\n}\nconst trackerKeys = new Set(trackerRows.map((row) => String(row['Gespräch-ID'] || '')).filter(Boolean));\nconst trackerReservationsFor = (trackerKey) => uniqueEvents.filter((event) => {\n  const eventDetail = details(event);\n  return event.event_type === 'manual_signal'\n    && eventDetail.boundary === 'validation_tracker'\n    && String(eventDetail.tracker_key || '') === trackerKey\n    && String(event.event_id || '') === 'validation-tracker:' + trackerKey + ':reservation:' + String(eventDetail.reservation_attempt);\n});\nconst explicitAnswerSignalsByLead = new Map();\nfor (const signal of manualSignals) {\n  const signalDetails = details(signal);\n  const explicitAnswer = String(signalDetails.signal_type || '').toLowerCase() === 'answer'\n    || String(signalDetails.kontaktstatus || '') === 'Antwort';\n  const signalTime = isoTimestamp(signal.occurred_at);\n  const leadId = String(signal.lead_id || '');\n  if (!explicitAnswer || !String(signal.event_id || '') || !leadId\n    || signalTime === null || nowTimestamp === null || signalTime > nowTimestamp) continue;\n  if (!explicitAnswerSignalsByLead.has(leadId)) explicitAnswerSignalsByLead.set(leadId, []);\n  explicitAnswerSignalsByLead.get(leadId).push(signal);\n}\nconst trackerRowFor = (lead, trackerKey, sourceEvent) => {\n  const sourceDetails = details(sourceEvent || {});\n  const sourceTime = isoTimestamp(sourceEvent?.occurred_at);\n  const occurredDate = sourceTime !== null && nowTimestamp !== null && sourceTime <= nowTimestamp\n    ? String(sourceEvent.occurred_at).slice(0, 10) : '';\n  return {\n    'Gespräch-ID': trackerKey,\n    Datum: occurredDate,\n    'Unternehmen / Organisation': String(lead.Unternehmen || ''),\n    Segment: String(lead.Segment || ''),\n    'Pipeline-Typ': String(lead['Pipeline-Typ'] || ''),\n    'Gesprächspartner / Rolle': [lead['Konkrete Kontaktperson'], lead.Zielrolle].filter(Boolean).join(' / '),\n    'Branche / Kontext': String(lead.Branche || ''),\n    'Konkreter Anlass': String(lead['Triggersignal / beobachtetes Problem'] || ''),\n    'Besprochenes Modul': String(sourceDetails.besprochenes_modul || sourceDetails['Besprochenes Modul'] || ''),\n    'Problemstärke 0–3': '', 'Dringlichkeit 0–3': '', 'Nutzenverständnis 0–3': '',\n    'Vertrauen 0–3': '', 'Modulpassung 0–3': '', 'Preis-/Budgetnähe 0–3': '',\n    'Sponsor/Entscheidung 0–3': '', 'Umsetzungsfähigkeit 0–3': '', 'Nächster Schritt 0–3': '',\n    Gesamtscore: '', Preisreaktion: '', 'Stärkster Einwand': '', Kaufsignal: '', Reaktionscodes: '',\n    'Nächster Schritt': String(lead['Nächster Schritt'] || ''),\n    Wiedervorlage: String(lead.Wiedervorlage || ''), Interviewstatus: '',\n  };\n};\nfor (const lead of leads) {\n  const leadId = String(lead['Lead-ID'] || '');\n  const explicitSignals = explicitAnswerSignalsByLead.get(leadId) || [];\n  const candidates = explicitSignals.length\n    ? explicitSignals.map((signal) => ({ trackerKey: String(signal.event_id), sourceEvent: signal }))\n    : String(lead.Kontaktstatus || '') === 'Antwort'\n      ? [{ trackerKey: 'crm-answer:' + leadId + ':epoch:' + activeEpoch(leadId), sourceEvent: null }]\n      : [];\n  for (const candidate of candidates) {\n    if (trackerKeys.has(candidate.trackerKey)) continue;\n    const reservationAttempt = trackerReservationsFor(candidate.trackerKey).length;\n    const actionId = 'validation-tracker:' + candidate.trackerKey + ':reservation:' + reservationAttempt;\n    actions.push({\n      kind: 'tracker_append', actionType: 'validation_tracker', actionId,\n      lifecycleId: 'validation-tracker:' + candidate.trackerKey,\n      trackerKey: candidate.trackerKey,\n      sourceEventId: String(candidate.sourceEvent?.event_id || candidate.trackerKey),\n      reservationAttempt, sequenceEpoch: activeEpoch(leadId), lead, runId: run.runId,\n      trackerRow: trackerRowFor(lead, candidate.trackerKey, candidate.sourceEvent),\n    });\n  }\n}\nconst context = {\n  today, now, holidays, leads, optOuts, openDraftLeadIds, manualSignals,\n};\nfor (const lead of leads) {\n  const leadId = String(lead['Lead-ID'] || '');\n  const sequenceEpoch = activeEpoch(leadId);\n  const decision = planLeadAction(lead, {\n    ...context, sequenceEpoch, sequenceStartedAt: sequenceStartedAt(leadId),\n  });\n  const attempt = Number(lead.Versuch || 0);\n  if (decision.action === 'create_draft') {\n    const lifecycleBase = ['draft', leadId, 'epoch', sequenceEpoch, 'attempt', attempt].join(':');\n    const cancellationCount = uniqueEvents.filter((event) => (\n      String(event.lead_id || '') === leadId\n      && (lifecycleId(event) === lifecycleBase || lifecycleId(event).startsWith(lifecycleBase + ':retry:'))\n      && recordByLifecycle.has(lifecycleId(event))\n      && validCancellationFor(event, recordByLifecycle.get(lifecycleId(event)))\n    )).length;\n    const lifecycle = cancellationCount ? lifecycleBase + ':retry:' + cancellationCount : lifecycleBase;\n    if (eventIds.has(lifecycle + ':reserved') || eventIds.has(lifecycle + ':created')) continue;\n    const rendered = renderTemplate(decision.templateKey, lead);\n    const subjectMarker = '[EAIR:' + leadId + ':' + sequenceEpoch + ':' + attempt + ':' + cancellationCount + ']';\n    actions.push({\n      kind: 'create_draft', actionType: \"draft_created\", actionId: lifecycle,\n      lifecycleId: lifecycle, sequenceEpoch, lead, decision, runId: run.runId,\n      draft: {\n        to: normalizeEmail(lead['E-Mail']),\n        subject: subjectMarker + ' ' + rendered.subject,\n        subjectMarker,\n        body: rendered.body,\n        missing: rendered.missing,\n        templateKey: decision.templateKey,\n      },\n    });\n  } else if (decision.action === 'close_sequence') {\n    const sequenceLifecycle = ['sequence', leadId, 'epoch', sequenceEpoch].join(':');\n    const actionId = sequenceLifecycle + ':closed';\n    if (!eventIds.has(actionId)) actions.push({\n      kind: 'writeback', actionType: \"sequence_closed\", actionId,\n      lifecycleId: sequenceLifecycle, lead, statePatch: decision.statePatch,\n      reason: decision.reason, runId: run.runId, eventDetails: { sequence_epoch: sequenceEpoch },\n    });\n  } else if (reportCategories[decision.reason]) {\n    actions.push({\n      kind: 'noop', actionType: 'diagnostic',\n      actionId: ['diagnostic', leadId, reportCategories[decision.reason]].join(':'),\n      lead, decision, runId: run.runId,\n      diagnostic: {\n        category: reportCategories[decision.reason],\n        fields: decision.reason === 'Pflichtfeld-Lücke' ? validateRequiredFields(lead) : [],\n      },\n    });\n  }\n}\n\nreturn (actions.length ? actions : [{ kind: 'noop', actionId: run.runId + ':noop', runId: run.runId }])\n  .map((action) => ({ json: action }));"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000015",
      "name": "Loop: Aktionen einzeln",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1180,
        -480
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000016",
      "name": "Switch: Aktion",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.3,
      "position": [
        1400,
        -340
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "00000000-0000-4000-8000-000000001601",
                    "leftValue": "={{ $json.kind }}",
                    "rightValue": "create_draft",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "draft"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "00000000-0000-4000-8000-000000001602",
                    "leftValue": "={{ $json.kind }}",
                    "rightValue": "writeback",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "writeback"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "00000000-0000-4000-8000-000000001603",
                    "leftValue": "={{ $json.kind }}",
                    "rightValue": "tracker_append",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "tracker"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "00000000-0000-4000-8000-000000001604",
                    "leftValue": "={{ $json.kind }}",
                    "rightValue": "label_recovery",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "label_recovery"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000017",
      "name": "Code: Draft reservieren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        -620
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const leadHeaders = [\"Unternehmen\",\"Branche\",\"Größe\",\"Region\",\"Segment\",\"Triggersignal / beobachtetes Problem\",\"Passendes EAIR-Einstiegsangebot\",\"Zielrolle\",\"Konkrete Kontaktperson\",\"Zugangskanal\",\"Problemstärke\",\"KI-/Transformationsanlass\",\"Ansprechpartner-Zugang\",\"Umsetzungsfähigkeit\",\"Angebotspassung\",\"Budgetnähe\",\"Beziehungsvorteil\",\"Gesamtscore\",\"Priorität\",\"Begründung\",\"Kontaktstatus\",\"Letzter Kontakt\",\"Nächster Schritt\",\"Wiedervorlage\",\"Einwände / Erkenntnisse\",\"Quelle\",\"Pipeline-Typ\",\"Follow-up-Stufe\",\"Terminstatus\",\"Opt-out / keine weitere Ansprache\",\"Lead-ID\",\"E-Mail\",\"Versuch\"];\nconst eventHeaders = [\"event_id\",\"run_id\",\"lead_id\",\"event_type\",\"status\",\"channel\",\"template_key\",\"subject_marker\",\"gmail_draft_id\",\"gmail_message_id\",\"gmail_thread_id\",\"occurred_at\",\"details_json\"];\nconst cell = (value) => ({ userEnteredValue: typeof value === 'number'\n  ? { numberValue: value }\n  : { stringValue: String(value ?? '') } });\nconst row = (headers, values) => ({ values: headers.map((header) => cell(values[header])) });\nconst action = $json;\nconst run = $('Code: Run-Kontext').first().json;\nconst occurredAt = $now.setZone('Europe/Berlin').toISO();\nconst eventId = action.lifecycleId + ':reserved';\nconst eventRows = [row(eventHeaders, {\n  event_id: eventId, run_id: run.runId, lead_id: action.lead['Lead-ID'],\n  event_type: \"draft_reserved\", status: action.lead.Kontaktstatus, channel: 'email',\n  template_key: action.draft.templateKey, subject_marker: action.draft.subjectMarker,\n  gmail_draft_id: '', gmail_message_id: '', gmail_thread_id: '', occurred_at: occurredAt,\n  details_json: JSON.stringify({\n    lifecycle_id: action.lifecycleId, sequence_epoch: action.sequenceEpoch, reason: action.decision.reason,\n  }),\n})];\nreturn { json: { ...action, eventId, eventRows } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000018",
      "name": "Sheets: Draft reservieren",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1840,
        -620
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{$('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID}}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: [{ appendCells: {\n  sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID), rows: $json.eventRows ?? [], fields: 'userEnteredValue'\n} }] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000034",
      "name": "Sheets: Events nach Reservierung neu laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2060,
        -620
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": false,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: event_id | run_id | lead_id | event_type | status | channel | template_key | subject_marker | gmail_draft_id | gmail_message_id | gmail_thread_id | occurred_at | details_json"
    },
    {
      "id": "00000000-0000-4000-8000-000000000035",
      "name": "Code: Draft-Autorität prüfen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2280,
        -620
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const action = $('Code: Draft reservieren').first().json;\nconst rows = $input.all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst details = (event) => {\n  try { return JSON.parse(event.details_json || '{}'); } catch { return {}; }\n};\nconst sequenceEpoch = String(action.sequenceEpoch || action.lifecycleId?.match(/:epoch:([^:]+)(?::|$)/u)?.[1] || '');\nconst owner = rows.find((event) => String(event.event_id || '') === String(action.eventId || ''));\nconst ownerDetails = details(owner || {});\nconst validOwner = owner?.event_type === \"draft_reserved\"\n  && String(owner.lead_id || '') === String(action.lead?.['Lead-ID'] || '')\n  && String(owner.subject_marker || '') === String(action.draft?.subjectMarker || '')\n  && String(ownerDetails.lifecycle_id || '') === String(action.lifecycleId || '')\n  && String(ownerDetails.sequence_epoch || '') === sequenceEpoch\n  && String(owner.run_id || '').length > 0;\nreturn { json: {\n  ...action,\n  reservationOwnerRunId: validOwner ? String(owner.run_id) : '',\n  isReservationOwner: validOwner && String(owner.run_id) === String(action.runId || ''),\n} };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000036",
      "name": "IF: Draft-Reservierung autoritativ",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2500,
        -620
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000003601",
              "leftValue": "={{ $json.isReservationOwner }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000037",
      "name": "Code: Draft-Reservierung verloren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        -500
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return { json: { ...$json, result: 'reservation_lost' } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000019",
      "name": "Gmail: Outreach als Entwurf",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2720,
        -700
      ],
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $('Code: Draft reservieren').item.json.draft.subject }}",
        "emailType": "text",
        "message": "={{ $('Code: Draft reservieren').item.json.draft.body }}",
        "options": {
          "sendTo": "={{ $('Code: Draft reservieren').item.json.draft.to }}"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000038",
      "name": "Gmail: EAIR-Entwurfslabel setzen",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2940,
        -700
      ],
      "parameters": {
        "resource": "message",
        "operation": "addLabels",
        "messageId": "={{ $('Gmail: Outreach als Entwurf').item.json.message.id }}",
        "labelIds": [
          "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_GMAIL_DRAFT_LABEL_ID }}"
        ]
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000020",
      "name": "Code: Draft-Erfolg vorbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3160,
        -700
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const leadHeaders = [\"Unternehmen\",\"Branche\",\"Größe\",\"Region\",\"Segment\",\"Triggersignal / beobachtetes Problem\",\"Passendes EAIR-Einstiegsangebot\",\"Zielrolle\",\"Konkrete Kontaktperson\",\"Zugangskanal\",\"Problemstärke\",\"KI-/Transformationsanlass\",\"Ansprechpartner-Zugang\",\"Umsetzungsfähigkeit\",\"Angebotspassung\",\"Budgetnähe\",\"Beziehungsvorteil\",\"Gesamtscore\",\"Priorität\",\"Begründung\",\"Kontaktstatus\",\"Letzter Kontakt\",\"Nächster Schritt\",\"Wiedervorlage\",\"Einwände / Erkenntnisse\",\"Quelle\",\"Pipeline-Typ\",\"Follow-up-Stufe\",\"Terminstatus\",\"Opt-out / keine weitere Ansprache\",\"Lead-ID\",\"E-Mail\",\"Versuch\"];\nconst eventHeaders = [\"event_id\",\"run_id\",\"lead_id\",\"event_type\",\"status\",\"channel\",\"template_key\",\"subject_marker\",\"gmail_draft_id\",\"gmail_message_id\",\"gmail_thread_id\",\"occurred_at\",\"details_json\"];\nconst cell = (value) => ({ userEnteredValue: typeof value === 'number'\n  ? { numberValue: value }\n  : { stringValue: String(value ?? '') } });\nconst row = (headers, values) => ({ values: headers.map((header) => cell(values[header])) });\nconst action = $('Code: Draft reservieren').item.json;\nconst gmail = $('Gmail: Outreach als Entwurf').item.json;\nconst run = $('Code: Run-Kontext').first().json;\nconst occurredAt = $now.setZone('Europe/Berlin').toISO();\nconst eventId = action.lifecycleId + ':created';\nconst eventRows = [row(eventHeaders, {\n  event_id: eventId, run_id: run.runId, lead_id: action.lead['Lead-ID'],\n  event_type: \"draft_created\", status: action.lead.Kontaktstatus, channel: 'email',\n  template_key: action.draft.templateKey, subject_marker: action.draft.subjectMarker,\n  gmail_draft_id: gmail.id || gmail.draftId || '',\n  gmail_message_id: gmail.message?.id || gmail.messageId || '',\n  gmail_thread_id: gmail.message?.threadId || gmail.threadId || '', occurred_at: occurredAt,\n  details_json: JSON.stringify({\n    lifecycle_id: action.lifecycleId, sequence_epoch: action.sequenceEpoch, reservation_event_id: action.eventId,\n  }),\n})];\nreturn { json: { ...action, gmail, eventId, eventRows } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000021",
      "name": "Sheets: Draft-Erfolg protokollieren",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        3380,
        -700
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{$('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID}}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: [{ appendCells: {\n  sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID), rows: $json.eventRows ?? [], fields: 'userEnteredValue'\n} }] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000022",
      "name": "Code: Draft abgeschlossen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3600,
        -700
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const action = $('Code: Draft-Erfolg vorbereiten').item.json; return { json: { ...action, result: 'draft_created' } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000023",
      "name": "Code: Writeback vorbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        -340
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const leadHeaders = [\"Unternehmen\",\"Branche\",\"Größe\",\"Region\",\"Segment\",\"Triggersignal / beobachtetes Problem\",\"Passendes EAIR-Einstiegsangebot\",\"Zielrolle\",\"Konkrete Kontaktperson\",\"Zugangskanal\",\"Problemstärke\",\"KI-/Transformationsanlass\",\"Ansprechpartner-Zugang\",\"Umsetzungsfähigkeit\",\"Angebotspassung\",\"Budgetnähe\",\"Beziehungsvorteil\",\"Gesamtscore\",\"Priorität\",\"Begründung\",\"Kontaktstatus\",\"Letzter Kontakt\",\"Nächster Schritt\",\"Wiedervorlage\",\"Einwände / Erkenntnisse\",\"Quelle\",\"Pipeline-Typ\",\"Follow-up-Stufe\",\"Terminstatus\",\"Opt-out / keine weitere Ansprache\",\"Lead-ID\",\"E-Mail\",\"Versuch\"];\nconst eventHeaders = [\"event_id\",\"run_id\",\"lead_id\",\"event_type\",\"status\",\"channel\",\"template_key\",\"subject_marker\",\"gmail_draft_id\",\"gmail_message_id\",\"gmail_thread_id\",\"occurred_at\",\"details_json\"];\nconst cell = (value) => ({ userEnteredValue: typeof value === 'number'\n  ? { numberValue: value }\n  : { stringValue: String(value ?? '') } });\nconst row = (headers, values) => ({ values: headers.map((header) => cell(values[header])) });\nconst action = $json;\nconst run = $('Code: Run-Kontext').first().json;\nconst occurredAt = $now.setZone('Europe/Berlin').toISO();\nconst updatedLead = action.lead && action.statePatch ? { ...action.lead, ...action.statePatch } : undefined;\nconst eventId = action.actionId;\nconst eventRows = [row(eventHeaders, {\n  event_id: eventId, run_id: run.runId, lead_id: action.lead?.['Lead-ID'] || action.sourceEvent?.lead_id || '',\n  event_type: action.actionType, status: updatedLead?.Kontaktstatus || action.lead?.Kontaktstatus || action.sourceEvent?.status || '',\n  channel: 'email', template_key: action.sourceEvent?.template_key || '',\n  subject_marker: action.sourceEvent?.subject_marker || '', gmail_draft_id: action.sourceEvent?.gmail_draft_id || '',\n  gmail_message_id: action.sourceEvent?.gmail_message_id || '', gmail_thread_id: action.sourceEvent?.gmail_thread_id || '',\n  occurred_at: occurredAt,\n  details_json: JSON.stringify({\n    lifecycle_id: action.lifecycleId || '', sequence_epoch: action.eventDetails?.sequence_epoch || '',\n    reason: action.reason || '', ...(action.eventDetails || {}),\n  }),\n})];\nconst rowNumber = Number(updatedLead?.row_number);\nif (updatedLead && (!Number.isInteger(rowNumber) || rowNumber < 2)) {\n  throw new Error('Gültige row_number für schmalen CRM-Patch erforderlich');\n}\nconst patchableHeaders = new Set(['Kontaktstatus', 'Versuch', 'Letzter Kontakt', 'Wiedervorlage', 'Einwände / Erkenntnisse']);\nconst patchEntries = Object.entries(action.statePatch || {});\nif (patchEntries.some(([header]) => !patchableHeaders.has(header))) {\n  throw new Error('Nicht freigegebene CRM-Patchspalte');\n}\nconst leadUpdates = updatedLead ? patchEntries\n  .map(([header, value]) => ({ columnIndex: leadHeaders.indexOf(header), header, value }))\n  .filter(({ columnIndex }) => columnIndex >= 0)\n  .sort((left, right) => left.columnIndex - right.columnIndex)\n  .map(({ columnIndex, value }) => ({\n    range: {\n      startRowIndex: rowNumber - 1, endRowIndex: rowNumber,\n      startColumnIndex: columnIndex, endColumnIndex: columnIndex + 1,\n    },\n    row: { values: [cell(value)] },\n  })) : [];\nreturn { json: { ...action, eventId, eventRows, leadUpdates } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000024",
      "name": "Sheets: Atomarer Writeback",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1840,
        -340
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{$('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID}}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: [\n  ...($json.leadUpdates ?? []).map((update) => ({ updateCells: {\n    range: { sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_LEADS_SHEET_ID), ...update.range },\n    rows: [update.row], fields: 'userEnteredValue'\n  } })),\n  { appendCells: {\n    sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID), rows: $json.eventRows ?? [], fields: 'userEnteredValue'\n  } }\n] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000025",
      "name": "Code: Writeback abgeschlossen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2060,
        -340
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const action = $('Code: Writeback vorbereiten').item.json; return { json: { ...action, result: action.resultType || action.actionType } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000026",
      "name": "Code: Noop",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        -100
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return { json: { ...$json, result: 'noop' } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000039",
      "name": "Code: Tracker reservieren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        -40
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const leadHeaders = [\"Unternehmen\",\"Branche\",\"Größe\",\"Region\",\"Segment\",\"Triggersignal / beobachtetes Problem\",\"Passendes EAIR-Einstiegsangebot\",\"Zielrolle\",\"Konkrete Kontaktperson\",\"Zugangskanal\",\"Problemstärke\",\"KI-/Transformationsanlass\",\"Ansprechpartner-Zugang\",\"Umsetzungsfähigkeit\",\"Angebotspassung\",\"Budgetnähe\",\"Beziehungsvorteil\",\"Gesamtscore\",\"Priorität\",\"Begründung\",\"Kontaktstatus\",\"Letzter Kontakt\",\"Nächster Schritt\",\"Wiedervorlage\",\"Einwände / Erkenntnisse\",\"Quelle\",\"Pipeline-Typ\",\"Follow-up-Stufe\",\"Terminstatus\",\"Opt-out / keine weitere Ansprache\",\"Lead-ID\",\"E-Mail\",\"Versuch\"];\nconst eventHeaders = [\"event_id\",\"run_id\",\"lead_id\",\"event_type\",\"status\",\"channel\",\"template_key\",\"subject_marker\",\"gmail_draft_id\",\"gmail_message_id\",\"gmail_thread_id\",\"occurred_at\",\"details_json\"];\nconst cell = (value) => ({ userEnteredValue: typeof value === 'number'\n  ? { numberValue: value }\n  : { stringValue: String(value ?? '') } });\nconst row = (headers, values) => ({ values: headers.map((header) => cell(values[header])) });\nconst action = $json;\nconst run = $('Code: Run-Kontext').first().json;\nconst occurredAt = $now.setZone('Europe/Berlin').toISO();\nconst eventId = action.actionId;\nconst eventRows = [row(eventHeaders, {\n  event_id: eventId, run_id: run.runId, lead_id: action.lead['Lead-ID'],\n  event_type: 'manual_signal', status: action.lead.Kontaktstatus, channel: 'sheet',\n  template_key: '', subject_marker: '', gmail_draft_id: '', gmail_message_id: '',\n  gmail_thread_id: '', occurred_at: occurredAt,\n  details_json: JSON.stringify({\n    boundary: 'validation_tracker', lifecycle_id: action.lifecycleId,\n    sequence_epoch: action.sequenceEpoch, source_event_id: action.sourceEventId,\n    tracker_key: action.trackerKey, reservation_attempt: action.reservationAttempt,\n  }),\n})];\nreturn { json: { ...action, runId: run.runId, eventId, eventRows } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000040",
      "name": "Sheets: Tracker reservieren",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1840,
        -40
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{$('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID}}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: [{ appendCells: {\n  sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID), rows: $json.eventRows ?? [], fields: 'userEnteredValue'\n} }] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000041",
      "name": "Sheets: Events für Tracker-Gate neu laden",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2060,
        -40
      ],
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_CRM_EVENTS_SHEET_ID }}",
          "mode": "id"
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "executeOnce": false,
      "alwaysOutputData": true,
      "onError": "continueErrorOutput",
      "notesInFlow": true,
      "notes": "Erwartete Header: event_id | run_id | lead_id | event_type | status | channel | template_key | subject_marker | gmail_draft_id | gmail_message_id | gmail_thread_id | occurred_at | details_json"
    },
    {
      "id": "00000000-0000-4000-8000-000000000042",
      "name": "Code: Tracker-Autorität prüfen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2280,
        -40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const action = $('Code: Tracker reservieren').first().json;\nconst rows = $input.all().map((item) => item.json).filter((value) => value && Object.keys(value).length);\nconst details = (event) => {\n  try { return JSON.parse(event.details_json || '{}'); } catch { return {}; }\n};\nconst lifecycleId = String(action.lifecycleId || 'validation-tracker:' + String(action.trackerKey || ''));\nconst owner = rows.find((event) => String(event.event_id || '') === String(action.eventId || ''));\nconst ownerDetails = details(owner || {});\nconst validOwner = owner?.event_type === 'manual_signal'\n  && String(owner.lead_id || '') === String(action.lead?.['Lead-ID'] || '')\n  && ownerDetails.boundary === 'validation_tracker'\n  && String(ownerDetails.lifecycle_id || '') === lifecycleId\n  && String(ownerDetails.sequence_epoch || '') === String(action.sequenceEpoch || '')\n  && String(ownerDetails.source_event_id || '') === String(action.sourceEventId || '')\n  && String(ownerDetails.tracker_key || '') === String(action.trackerKey || '')\n  && Number(ownerDetails.reservation_attempt) === Number(action.reservationAttempt)\n  && String(owner.run_id || '').length > 0;\nreturn { json: {\n  ...action,\n  reservationOwnerRunId: validOwner ? String(owner.run_id) : '',\n  isReservationOwner: validOwner && String(owner.run_id) === String(action.runId || ''),\n} };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000043",
      "name": "IF: Tracker-Reservierung autoritativ",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2500,
        -40
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "00000000-0000-4000-8000-000000004301",
              "leftValue": "={{ $json.isReservationOwner }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000044",
      "name": "Sheets: Validierungszeile upserten",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2720,
        -120
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_VALIDATION_SPREADSHEET_ID }}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_VALIDATION_TAB_NAME }}",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Gespräch-ID": "={{ $json.trackerRow[\"Gespräch-ID\"] }}",
            "Datum": "={{ $json.trackerRow[\"Datum\"] }}",
            "Unternehmen / Organisation": "={{ $json.trackerRow[\"Unternehmen / Organisation\"] }}",
            "Segment": "={{ $json.trackerRow[\"Segment\"] }}",
            "Pipeline-Typ": "={{ $json.trackerRow[\"Pipeline-Typ\"] }}",
            "Gesprächspartner / Rolle": "={{ $json.trackerRow[\"Gesprächspartner / Rolle\"] }}",
            "Branche / Kontext": "={{ $json.trackerRow[\"Branche / Kontext\"] }}",
            "Konkreter Anlass": "={{ $json.trackerRow[\"Konkreter Anlass\"] }}",
            "Besprochenes Modul": "={{ $json.trackerRow[\"Besprochenes Modul\"] }}",
            "Problemstärke 0–3": "={{ $json.trackerRow[\"Problemstärke 0–3\"] }}",
            "Dringlichkeit 0–3": "={{ $json.trackerRow[\"Dringlichkeit 0–3\"] }}",
            "Nutzenverständnis 0–3": "={{ $json.trackerRow[\"Nutzenverständnis 0–3\"] }}",
            "Vertrauen 0–3": "={{ $json.trackerRow[\"Vertrauen 0–3\"] }}",
            "Modulpassung 0–3": "={{ $json.trackerRow[\"Modulpassung 0–3\"] }}",
            "Preis-/Budgetnähe 0–3": "={{ $json.trackerRow[\"Preis-/Budgetnähe 0–3\"] }}",
            "Sponsor/Entscheidung 0–3": "={{ $json.trackerRow[\"Sponsor/Entscheidung 0–3\"] }}",
            "Umsetzungsfähigkeit 0–3": "={{ $json.trackerRow[\"Umsetzungsfähigkeit 0–3\"] }}",
            "Nächster Schritt 0–3": "={{ $json.trackerRow[\"Nächster Schritt 0–3\"] }}",
            "Gesamtscore": "={{ $json.trackerRow[\"Gesamtscore\"] }}",
            "Preisreaktion": "={{ $json.trackerRow[\"Preisreaktion\"] }}",
            "Stärkster Einwand": "={{ $json.trackerRow[\"Stärkster Einwand\"] }}",
            "Kaufsignal": "={{ $json.trackerRow[\"Kaufsignal\"] }}",
            "Reaktionscodes": "={{ $json.trackerRow[\"Reaktionscodes\"] }}",
            "Nächster Schritt": "={{ $json.trackerRow[\"Nächster Schritt\"] }}",
            "Wiedervorlage": "={{ $json.trackerRow[\"Wiedervorlage\"] }}",
            "Interviewstatus": "={{ $json.trackerRow[\"Interviewstatus\"] }}"
          },
          "matchingColumns": [
            "Gespräch-ID"
          ],
          "schema": [
            {
              "id": "Gespräch-ID",
              "displayName": "Gespräch-ID",
              "required": false,
              "defaultMatch": true,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Datum",
              "displayName": "Datum",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Unternehmen / Organisation",
              "displayName": "Unternehmen / Organisation",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Segment",
              "displayName": "Segment",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Pipeline-Typ",
              "displayName": "Pipeline-Typ",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Gesprächspartner / Rolle",
              "displayName": "Gesprächspartner / Rolle",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Branche / Kontext",
              "displayName": "Branche / Kontext",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Konkreter Anlass",
              "displayName": "Konkreter Anlass",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Besprochenes Modul",
              "displayName": "Besprochenes Modul",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Problemstärke 0–3",
              "displayName": "Problemstärke 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Dringlichkeit 0–3",
              "displayName": "Dringlichkeit 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Nutzenverständnis 0–3",
              "displayName": "Nutzenverständnis 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Vertrauen 0–3",
              "displayName": "Vertrauen 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Modulpassung 0–3",
              "displayName": "Modulpassung 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Preis-/Budgetnähe 0–3",
              "displayName": "Preis-/Budgetnähe 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Sponsor/Entscheidung 0–3",
              "displayName": "Sponsor/Entscheidung 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Umsetzungsfähigkeit 0–3",
              "displayName": "Umsetzungsfähigkeit 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Nächster Schritt 0–3",
              "displayName": "Nächster Schritt 0–3",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Gesamtscore",
              "displayName": "Gesamtscore",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Preisreaktion",
              "displayName": "Preisreaktion",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Stärkster Einwand",
              "displayName": "Stärkster Einwand",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Kaufsignal",
              "displayName": "Kaufsignal",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Reaktionscodes",
              "displayName": "Reaktionscodes",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Nächster Schritt",
              "displayName": "Nächster Schritt",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Wiedervorlage",
              "displayName": "Wiedervorlage",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            },
            {
              "id": "Interviewstatus",
              "displayName": "Interviewstatus",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string",
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000045",
      "name": "Code: Tracker abgeschlossen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2940,
        -120
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const action = $('Code: Tracker-Autorität prüfen').item.json; return { json: { ...action, result: 'tracker_appended' } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000046",
      "name": "Code: Tracker-Reservierung verloren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2720,
        40
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return { json: { ...$json, result: 'reservation_lost' } };"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000047",
      "name": "Gmail: Wiedergefundenes Entwurfslabel setzen",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        1620,
        160
      ],
      "parameters": {
        "resource": "message",
        "operation": "addLabels",
        "messageId": "={{ $json.sourceEvent.gmail_message_id }}",
        "labelIds": [
          "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_GMAIL_DRAFT_LABEL_ID }}"
        ]
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000027",
      "name": "Code: Runs-Logging",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        -760
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const runHeaders = [\"run_id\",\"started_at\",\"finished_at\",\"trigger\",\"status\",\"draft_count\",\"error_stage\",\"error_message\"];\nconst cell = (value) => ({ userEnteredValue: typeof value === 'number'\n  ? { numberValue: value }\n  : { stringValue: String(value ?? '') } });\nconst row = (headers, values) => ({ values: headers.map((header) => cell(values[header])) });\nconst processed = $input.all().map((item) => item.json);\nconst run = $('Code: Run-Kontext').first().json;\nconst finishedAt = $now.setZone('Europe/Berlin').toISO();\nconst draftCount = processed.filter((item) => item.kind === 'create_draft' && item.result === 'draft_created').length;\nconst runRows = [row(runHeaders, {\n  run_id: run.runId, started_at: run.startedAt, finished_at: finishedAt,\n  trigger: run.trigger, status: 'completed', draft_count: draftCount,\n  error_stage: '', error_message: '',\n})];\nreturn [{ json: { processed, runRows, run: { ...run, finishedAt, status: 'completed', draftCount } } }];"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000028",
      "name": "Sheets: Run protokollieren",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1620,
        -760
      ],
      "parameters": {
        "method": "POST",
        "url": "=https://sheets.googleapis.com/v4/spreadsheets/{{$('Code: Runtime-Konfiguration').first().json.EAIR_CRM_SPREADSHEET_ID}}:batchUpdate",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleSheetsOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ requests: [{ appendCells: {\n  sheetId: Number($('Code: Runtime-Konfiguration').first().json.EAIR_CRM_RUNS_SHEET_ID), rows: $json.runRows ?? [], fields: 'userEnteredValue'\n} }] }) }}",
        "options": {
          "timeout": 30000
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000029",
      "name": "Code: Bericht erzeugen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1840,
        -760
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const buildRunReport = function buildRunReport(processed, run) {\n  const items = Array.isArray(processed) ? processed : [];\n  const label = (item) => {\n    const lead = item.lead || {};\n    const company = String(lead.Unternehmen || item.sourceEvent?.lead_id || 'Unbekannter Lead');\n    const email = String(lead['E-Mail'] || 'keine E-Mail');\n    return `${company} <${email}>`;\n  };\n  const unique = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right, 'de'));\n  const diagnostic = (category) => items.filter((item) => item.diagnostic?.category === category);\n  const optOuts = unique(diagnostic('opt_out').map(label));\n  const duplicates = unique(diagnostic('duplicate').map(label));\n  const required = unique(diagnostic('required_fields').map((item) => (\n    `${label(item)} [${(item.diagnostic.fields || []).join(', ')}]`\n  )));\n  const missing = unique(items.flatMap((item) => (\n    item.kind === 'create_draft' && item.draft?.missing?.length\n      ? [`${label(item)} [${item.draft.missing.join(', ')}]`]\n      : []\n  )));\n  const stale = unique(items.flatMap((item) => (\n    item.actionType === 'escalate' && item.reason === 'Entwurf liegt'\n      ? [`${label(item)} [${item.sourceEvent?.gmail_draft_id || item.sourceEvent?.gmail_message_id || item.lifecycleId || 'ohne Gmail-ID'}]`]\n      : []\n  )));\n  const recoveryPending = unique(diagnostic('recovery_pending').map((item) => (\n    `${label(item)} [Abbruch-ID: ${item.diagnostic.cancellationEventId || 'nicht verfügbar'}]`\n  )));\n  const recoveryConflicts = unique(diagnostic('recovery_conflict').map((item) => (\n    `${label(item)} [Autoritativ: ${item.diagnostic.authoritativeLifecycleId || 'unbekannt'}; `\n    + `Konflikt: ${item.diagnostic.conflictingLifecycleId || 'unbekannt'}; `\n    + `Marker: ${item.diagnostic.authoritativeSubjectMarker || 'unbekannt'} / `\n    + `${item.diagnostic.conflictingSubjectMarker || 'unbekannt'}; `\n    + `Abbruch-ID: ${item.diagnostic.cancellationEventId || 'nicht verfügbar'}; `\n    + `${item.diagnostic.cleanupInstruction || 'Konflikt manuell prüfen'}]`\n  )));\n  const draftCount = Number.isInteger(run?.draftCount)\n    ? run.draftCount\n    : items.filter((item) => item.kind === 'create_draft' && item.result === 'draft_created').length;\n  const categories = [\n    ['Opt-outs', optOuts],\n    ['Dubletten', duplicates],\n    ['Pflichtfeld-Lücken', required],\n    ['Fehlende Vorlagenwerte', missing],\n    ['Liegengebliebene Entwürfe', stale],\n    ['Ungeklärte Reservierungen', recoveryPending],\n    ['Recovery-Konflikte', recoveryConflicts],\n  ];\n  const blockedLeadCount = optOuts.length + duplicates.length + required.length;\n  const lines = [\n    'Der CRM-Lauf wurde abgeschlossen.',\n    `Erzeugte Entwürfe: ${draftCount}`,\n    ...categories.map(([name, values]) => `${name} (${values.length}): ${values.length ? values.join('; ') : 'keine'}`),\n    'Automatisch versendete Nachrichten: 0',\n    'Versand bleibt manuell.',\n  ];\n  return {\n    subject: `[EAIR] CRM-Lauf: ${draftCount} ${draftCount === 1 ? 'Entwurf' : 'Entwürfe'}, `\n      + `${blockedLeadCount} ${blockedLeadCount === 1 ? 'blockierter Lead' : 'blockierte Leads'}, `\n      + '0 automatische Sendungen',\n    body: lines.join('\\n'),\n  };\n};\nconst logging = $('Code: Runs-Logging').first().json;\nreturn [{ json: { report: buildRunReport(logging.processed, logging.run) } }];"
      }
    },
    {
      "id": "00000000-0000-4000-8000-000000000030",
      "name": "Gmail: Bericht als Entwurf",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [
        2060,
        -760
      ],
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "={{ $json.report.subject }}",
        "emailType": "text",
        "message": "={{ $json.report.body }}",
        "options": {
          "sendTo": "={{ $('Code: Runtime-Konfiguration').first().json.EAIR_REPORT_RECIPIENT }}"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "00000000-0000-4000-8000-000000000031",
      "name": "Stop and Error: Lauf abbrechen",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1.1,
      "position": [
        2280,
        -100
      ],
      "parameters": {
        "errorMessage": "CRM-Lauf abgebrochen; Details stehen im Fehlerworkflow."
      }
    }
  ],
  "connections": {
    "Schedule: Werktags 07:00": {
      "main": [
        [
          {
            "node": "Code: Runtime-Konfiguration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook: Manueller Lauf": {
      "main": [
        [
          {
            "node": "Code: Runtime-Konfiguration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Runtime-Konfiguration": {
      "main": [
        [
          {
            "node": "IF: Konfiguration vollständig",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Konfiguration vollständig": {
      "main": [
        [
          {
            "node": "Code: Run-Kontext",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Run-Kontext": {
      "main": [
        [
          {
            "node": "IF: Webhook-Lauf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Webhook-Lauf": {
      "main": [
        [
          {
            "node": "Respond: Lauf angenommen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Sheets: Leads laden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond: Lauf angenommen": {
      "main": [
        [
          {
            "node": "Sheets: Leads laden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Leads laden": {
      "main": [
        [
          {
            "node": "Sheets: Opt-outs laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Opt-outs laden": {
      "main": [
        [
          {
            "node": "Sheets: Events laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Events laden": {
      "main": [
        [
          {
            "node": "Sheets: Feiertage laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Feiertage laden": {
      "main": [
        [
          {
            "node": "Sheets: Validierungstracker laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Validierungstracker laden": {
      "main": [
        [
          {
            "node": "Gmail: Offene Entwürfe laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Offene Entwürfe laden": {
      "main": [
        [
          {
            "node": "Gmail: Gesendete Nachrichten laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Gesendete Nachrichten laden": {
      "main": [
        [
          {
            "node": "Code: Leads entscheiden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Leads entscheiden": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop: Aktionen einzeln": {
      "main": [
        [
          {
            "node": "Code: Runs-Logging",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Switch: Aktion",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch: Aktion": {
      "main": [
        [
          {
            "node": "Code: Draft reservieren",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code: Writeback vorbereiten",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code: Tracker reservieren",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail: Wiedergefundenes Entwurfslabel setzen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code: Noop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Draft reservieren": {
      "main": [
        [
          {
            "node": "Sheets: Draft reservieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Draft reservieren": {
      "main": [
        [
          {
            "node": "Sheets: Events nach Reservierung neu laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Events nach Reservierung neu laden": {
      "main": [
        [
          {
            "node": "Code: Draft-Autorität prüfen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Draft-Autorität prüfen": {
      "main": [
        [
          {
            "node": "IF: Draft-Reservierung autoritativ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Draft-Reservierung autoritativ": {
      "main": [
        [
          {
            "node": "Gmail: Outreach als Entwurf",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code: Draft-Reservierung verloren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Draft-Reservierung verloren": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Outreach als Entwurf": {
      "main": [
        [
          {
            "node": "Gmail: EAIR-Entwurfslabel setzen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: EAIR-Entwurfslabel setzen": {
      "main": [
        [
          {
            "node": "Code: Draft-Erfolg vorbereiten",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Draft-Erfolg vorbereiten": {
      "main": [
        [
          {
            "node": "Sheets: Draft-Erfolg protokollieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Draft-Erfolg protokollieren": {
      "main": [
        [
          {
            "node": "Code: Draft abgeschlossen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Draft abgeschlossen": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Writeback vorbereiten": {
      "main": [
        [
          {
            "node": "Sheets: Atomarer Writeback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Atomarer Writeback": {
      "main": [
        [
          {
            "node": "Code: Writeback abgeschlossen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Writeback abgeschlossen": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Tracker reservieren": {
      "main": [
        [
          {
            "node": "Sheets: Tracker reservieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Tracker reservieren": {
      "main": [
        [
          {
            "node": "Sheets: Events für Tracker-Gate neu laden",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Events für Tracker-Gate neu laden": {
      "main": [
        [
          {
            "node": "Code: Tracker-Autorität prüfen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Tracker-Autorität prüfen": {
      "main": [
        [
          {
            "node": "IF: Tracker-Reservierung autoritativ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF: Tracker-Reservierung autoritativ": {
      "main": [
        [
          {
            "node": "Sheets: Validierungszeile upserten",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Code: Tracker-Reservierung verloren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Validierungszeile upserten": {
      "main": [
        [
          {
            "node": "Code: Tracker abgeschlossen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Tracker abgeschlossen": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Tracker-Reservierung verloren": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Wiedergefundenes Entwurfslabel setzen": {
      "main": [
        [
          {
            "node": "Code: Writeback vorbereiten",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Noop": {
      "main": [
        [
          {
            "node": "Loop: Aktionen einzeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Runs-Logging": {
      "main": [
        [
          {
            "node": "Sheets: Run protokollieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sheets: Run protokollieren": {
      "main": [
        [
          {
            "node": "Code: Bericht erzeugen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code: Bericht erzeugen": {
      "main": [
        [
          {
            "node": "Gmail: Bericht als Entwurf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Bericht als Entwurf": {
      "main": [
        [],
        [
          {
            "node": "Stop and Error: Lauf abbrechen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Berlin"
  },
  "tags": []
}
