{
  "name": "Meeting-Recording → Transkript → KI To-Do-Liste → E-Mail an Teilnehmer",
  "nodes": [
    {
      "id": "webhook-1",
      "name": "Meeting-Recording empfangen",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [250, 300],
      "webhookId": "meeting-recording",
      "parameters": {
        "httpMethod": "POST",
        "path": "meeting-recording",
        "responseMode": "onReceived",
        "options": {
          "binaryData": true,
          "rawBody": true
        }
      },
      "notes": "Empfängt das Meeting-Recording per POST.\n\nOption A: Audio/Video-Datei als Binary (von Zoom, Teams, Google Meet via Zapier/Make/direkter Upload)\nOption B: JSON mit bereits fertigem Transkript:\n{\n  \"meeting_title\": \"Sprint Planning\",\n  \"date\": \"2026-02-17\",\n  \"participants\": [{\"name\": \"Max Müller\", \"email\": \"max@firma.de\"}],\n  \"transcript\": \"...\",\n  \"recording_url\": \"https://...\"\n}\n\nViele Tools (Zoom, Fireflies, Otter.ai) können Webhooks senden."
    },
    {
      "id": "check-input",
      "name": "Transkript oder Audio?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [500, 300],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "has-transcript",
              "leftValue": "={{ $json.body?.transcript || $json.transcript || '' }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "notes": "Prüft ob bereits ein Transkript mitgeliefert wurde (z.B. von Otter.ai, Fireflies, Zoom) oder ob eine Audio-Datei transkribiert werden muss."
    },
    {
      "id": "use-transcript",
      "name": "Vorhandenes Transkript nutzen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [750, 150],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst body = data.body || data;\n\nreturn [{\n  json: {\n    meeting_title: body.meeting_title || body.topic || 'Unbenanntes Meeting',\n    date: body.date || body.start_time || new Date().toISOString().split('T')[0],\n    participants: body.participants || body.attendees || [],\n    transcript: body.transcript || body.text || '',\n    recording_url: body.recording_url || body.play_url || '',\n    duration_minutes: body.duration || body.duration_minutes || 0,\n    quelle: 'Mitgeliefertes Transkript'\n  }\n}];"
      },
      "notes": "Extrahiert und normalisiert das mitgelieferte Transkript und die Meeting-Metadaten."
    },
    {
      "id": "whisper-transcribe",
      "name": "Audio transkribieren (OpenAI Whisper)",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [750, 450],
      "parameters": {
        "resource": "audio",
        "operation": "transcribe",
        "options": {
          "language": "de",
          "temperature": 0
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "",
          "name": "OpenAI API"
        }
      },
      "notes": "Transkribiert die Audio-Datei via OpenAI Whisper API. Unterstützt mp3, mp4, mpeg, mpga, m4a, wav, webm. Sprache auf Deutsch gesetzt. Max. 25 MB pro Datei — bei längeren Recordings muss die Datei vorher gesplittet werden."
    },
    {
      "id": "prepare-audio-result",
      "name": "Whisper-Ergebnis aufbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1000, 450],
      "parameters": {
        "jsCode": "const whisperResult = $input.first().json;\nconst webhookData = $('Meeting-Recording empfangen').first().json;\nconst body = webhookData.body || webhookData;\n\nreturn [{\n  json: {\n    meeting_title: body.meeting_title || body.topic || 'Unbenanntes Meeting',\n    date: body.date || new Date().toISOString().split('T')[0],\n    participants: body.participants || body.attendees || [],\n    transcript: whisperResult.text || whisperResult.transcription || '',\n    recording_url: body.recording_url || '',\n    duration_minutes: body.duration || 0,\n    quelle: 'OpenAI Whisper Transkription'\n  }\n}];"
      },
      "notes": "Kombiniert das Whisper-Transkript mit den Meeting-Metadaten aus dem Webhook."
    },
    {
      "id": "merge-paths",
      "name": "Pfade zusammenführen",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.1,
      "position": [1250, 300],
      "parameters": {
        "mode": "passThrough",
        "output": "input1"
      },
      "notes": "Führt beide Pfade (vorhandenes Transkript / frisch transkribiert) zusammen."
    },
    {
      "id": "ai-analyze",
      "name": "KI: Meeting analysieren & To-Dos erstellen",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [1500, 300],
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "gpt-4o"
        },
        "messages": {
          "values": [
            {
              "content": "=Du bist ein erfahrener Meeting-Analyst und Projektmanager. Analysiere das folgende Meeting-Transkript gründlich.\n\n## Erstelle folgende Abschnitte:\n\n### 1. Meeting-Zusammenfassung\n- 3-5 Sätze Kernzusammenfassung\n- Was war das Hauptziel des Meetings?\n- Wurde das Ziel erreicht?\n\n### 2. Wichtigste Entscheidungen\n- Liste aller getroffenen Entscheidungen\n- Wer hat die Entscheidung getroffen?\n\n### 3. To-Do-Liste\nFür JEDE Aufgabe extrahiere:\n- **aufgabe**: Klare, aktionsorientierte Beschreibung\n- **verantwortlich**: Name der zuständigen Person (aus dem Transkript)\n- **deadline**: Genanntes oder geschätztes Datum (YYYY-MM-DD)\n- **prioritaet**: hoch / mittel / niedrig\n- **kontext**: Kurzer Kontext warum die Aufgabe wichtig ist\n\n### 4. Offene Fragen\n- Ungeklärte Punkte die Follow-up brauchen\n\n### 5. Nächstes Meeting\n- Wurde ein Folgetermin besprochen?\n\n## Teilnehmerliste (falls bekannt):\n{{ $json.participants ? JSON.stringify($json.participants) : 'Nicht angegeben — bitte aus dem Transkript erkennen' }}\n\n## Antwortformat (STRIKT als JSON):\n{\n  \"zusammenfassung\": \"\",\n  \"hauptziel\": \"\",\n  \"ziel_erreicht\": true/false,\n  \"entscheidungen\": [\n    {\"entscheidung\": \"\", \"entscheider\": \"\"}\n  ],\n  \"todos\": [\n    {\n      \"aufgabe\": \"\",\n      \"verantwortlich\": \"\",\n      \"deadline\": \"\",\n      \"prioritaet\": \"\",\n      \"kontext\": \"\"\n    }\n  ],\n  \"offene_fragen\": [\"\"],\n  \"naechstes_meeting\": \"\",\n  \"erkannte_teilnehmer\": [\n    {\"name\": \"\", \"rolle\": \"\"}\n  ],\n  \"stimmung\": \"\",\n  \"dauer_effektiv_genutzt\": \"\"\n}\n\n## Meeting-Daten:\nTitel: {{ $json.meeting_title }}\nDatum: {{ $json.date }}\nDauer: {{ $json.duration_minutes }} Minuten\n\n## Transkript:\n{{ $json.transcript }}"
            }
          ]
        },
        "options": {
          "temperature": 0.2,
          "maxTokens": 4000
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "",
          "name": "OpenAI API"
        }
      },
      "notes": "GPT-4o analysiert das Transkript und erstellt: Zusammenfassung, Entscheidungen, To-Do-Liste mit Verantwortlichen & Deadlines, offene Fragen, und Stimmungseinschätzung."
    },
    {
      "id": "parse-results",
      "name": "Ergebnis parsen & E-Mail bauen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1750, 300],
      "parameters": {
        "jsCode": "const aiResponse = $input.first().json.message.content;\nconst meetingData = $('Pfade zusammenführen').first().json;\n\ntry {\n  let jsonStr = aiResponse;\n  const jsonMatch = aiResponse.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) jsonStr = jsonMatch[0];\n  \n  const result = JSON.parse(jsonStr);\n  \n  // Build participant email list\n  let teilnehmerEmails = [];\n  if (meetingData.participants && Array.isArray(meetingData.participants)) {\n    teilnehmerEmails = meetingData.participants\n      .filter(p => p.email)\n      .map(p => p.email);\n  }\n  \n  // Build HTML email\n  const todosHtml = (result.todos || []).map((t, i) => {\n    const prioColor = t.prioritaet === 'hoch' ? '#e74c3c' : t.prioritaet === 'mittel' ? '#f39c12' : '#27ae60';\n    const prioLabel = t.prioritaet === 'hoch' ? '🔴 HOCH' : t.prioritaet === 'mittel' ? '🟡 MITTEL' : '🟢 NIEDRIG';\n    return `<tr>\n      <td style=\"padding:8px;border-bottom:1px solid #eee;\">${i + 1}</td>\n      <td style=\"padding:8px;border-bottom:1px solid #eee;\"><strong>${t.aufgabe}</strong><br><small style=\"color:#666\">${t.kontext || ''}</small></td>\n      <td style=\"padding:8px;border-bottom:1px solid #eee;\">${t.verantwortlich}</td>\n      <td style=\"padding:8px;border-bottom:1px solid #eee;\">${t.deadline || 'TBD'}</td>\n      <td style=\"padding:8px;border-bottom:1px solid #eee;\"><span style=\"color:${prioColor};font-weight:bold\">${prioLabel}</span></td>\n    </tr>`;\n  }).join('');\n  \n  const entscheidungenHtml = (result.entscheidungen || []).map(e => \n    `<li><strong>${e.entscheidung}</strong> <em>(${e.entscheider})</em></li>`\n  ).join('');\n  \n  const offeneFragenHtml = (result.offene_fragen || []).filter(f => f).map(f => \n    `<li>${f}</li>`\n  ).join('');\n  \n  const emailHtml = `\n<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"></head>\n<body style=\"font-family:Arial,sans-serif;max-width:700px;margin:0 auto;color:#333;\">\n  \n  <div style=\"background:#2c3e50;color:white;padding:20px;border-radius:8px 8px 0 0;\">\n    <h1 style=\"margin:0;font-size:22px;\">📋 Meeting-Protokoll</h1>\n    <p style=\"margin:5px 0 0;opacity:0.9;\">${meetingData.meeting_title} — ${meetingData.date}</p>\n  </div>\n  \n  <div style=\"padding:20px;background:#f9f9f9;\">\n    \n    <div style=\"background:white;padding:15px;border-radius:6px;margin-bottom:15px;border-left:4px solid #3498db;\">\n      <h2 style=\"margin:0 0 10px;font-size:16px;color:#2c3e50;\">📝 Zusammenfassung</h2>\n      <p style=\"margin:0;line-height:1.6;\">${result.zusammenfassung}</p>\n      ${result.ziel_erreicht !== undefined ? `<p style=\"margin:10px 0 0;\"><strong>Hauptziel:</strong> ${result.hauptziel} — ${result.ziel_erreicht ? '✅ Erreicht' : '⏳ Offen'}</p>` : ''}\n    </div>\n    \n    ${entscheidungenHtml ? `\n    <div style=\"background:white;padding:15px;border-radius:6px;margin-bottom:15px;border-left:4px solid #27ae60;\">\n      <h2 style=\"margin:0 0 10px;font-size:16px;color:#2c3e50;\">✅ Entscheidungen</h2>\n      <ul style=\"margin:0;padding-left:20px;\">${entscheidungenHtml}</ul>\n    </div>` : ''}\n    \n    <div style=\"background:white;padding:15px;border-radius:6px;margin-bottom:15px;border-left:4px solid #e74c3c;\">\n      <h2 style=\"margin:0 0 10px;font-size:16px;color:#2c3e50;\">🎯 To-Do-Liste (${(result.todos || []).length} Aufgaben)</h2>\n      <table style=\"width:100%;border-collapse:collapse;font-size:14px;\">\n        <thead>\n          <tr style=\"background:#f5f5f5;\">\n            <th style=\"padding:8px;text-align:left;border-bottom:2px solid #ddd;\">#</th>\n            <th style=\"padding:8px;text-align:left;border-bottom:2px solid #ddd;\">Aufgabe</th>\n            <th style=\"padding:8px;text-align:left;border-bottom:2px solid #ddd;\">Verantwortlich</th>\n            <th style=\"padding:8px;text-align:left;border-bottom:2px solid #ddd;\">Deadline</th>\n            <th style=\"padding:8px;text-align:left;border-bottom:2px solid #ddd;\">Prio</th>\n          </tr>\n        </thead>\n        <tbody>${todosHtml}</tbody>\n      </table>\n    </div>\n    \n    ${offeneFragenHtml ? `\n    <div style=\"background:white;padding:15px;border-radius:6px;margin-bottom:15px;border-left:4px solid #f39c12;\">\n      <h2 style=\"margin:0 0 10px;font-size:16px;color:#2c3e50;\">❓ Offene Fragen</h2>\n      <ul style=\"margin:0;padding-left:20px;\">${offeneFragenHtml}</ul>\n    </div>` : ''}\n    \n    ${result.naechstes_meeting ? `\n    <div style=\"background:white;padding:15px;border-radius:6px;margin-bottom:15px;border-left:4px solid #9b59b6;\">\n      <h2 style=\"margin:0 0 10px;font-size:16px;color:#2c3e50;\">📅 Nächstes Meeting</h2>\n      <p style=\"margin:0;\">${result.naechstes_meeting}</p>\n    </div>` : ''}\n    \n    <div style=\"text-align:center;padding:15px;color:#999;font-size:12px;\">\n      <p>Automatisch erstellt von der Meeting-Pipeline • ${new Date().toLocaleDateString('de-DE')}</p>\n      ${meetingData.recording_url ? `<p><a href=\"${meetingData.recording_url}\" style=\"color:#3498db;\">🎥 Recording ansehen</a></p>` : ''}\n    </div>\n    \n  </div>\n</body>\n</html>`;\n\n  // Plain text version for Slack / logs\n  const todosText = (result.todos || []).map((t, i) => \n    `${i + 1}. [${t.prioritaet?.toUpperCase()}] ${t.aufgabe} → ${t.verantwortlich} (bis ${t.deadline || 'TBD'})`\n  ).join('\\n');\n\n  return [{\n    json: {\n      // Meeting-Daten\n      meeting_title: meetingData.meeting_title,\n      meeting_date: meetingData.date,\n      recording_url: meetingData.recording_url || '',\n      \n      // KI-Analyse\n      zusammenfassung: result.zusammenfassung,\n      hauptziel: result.hauptziel,\n      ziel_erreicht: result.ziel_erreicht,\n      entscheidungen: JSON.stringify(result.entscheidungen || []),\n      todos: JSON.stringify(result.todos || []),\n      todos_text: todosText,\n      todos_anzahl: (result.todos || []).length,\n      offene_fragen: JSON.stringify(result.offene_fragen || []),\n      naechstes_meeting: result.naechstes_meeting || '',\n      stimmung: result.stimmung || '',\n      \n      // E-Mail\n      email_empfaenger: teilnehmerEmails.join(', '),\n      email_betreff: `📋 Protokoll & To-Dos: ${meetingData.meeting_title} (${meetingData.date})`,\n      email_html: emailHtml,\n      \n      // Meta\n      teilnehmer_erkannt: JSON.stringify(result.erkannte_teilnehmer || []),\n      erfasst_am: new Date().toISOString()\n    }\n  }];\n} catch (e) {\n  return [{\n    json: {\n      error: true,\n      message: 'Meeting-Analyse fehlgeschlagen: ' + e.message,\n      raw_response: aiResponse,\n      meeting_title: meetingData.meeting_title || 'Unbekannt'\n    }\n  }];\n}"
      },
      "notes": "Parst die KI-Analyse und baut eine professionell formatierte HTML-E-Mail mit farbcodierten Prioritäten, Entscheidungsliste und To-Do-Tabelle."
    },
    {
      "id": "if-error",
      "name": "Analyse OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [2000, 300],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "check-error",
              "leftValue": "={{ $json.error }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notTrue"
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "sheets-protokoll",
      "name": "Protokoll-Archiv (Google Sheets)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [2250, 150],
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "GOOGLE_SHEET_URL_HIER_EINFUEGEN"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Meeting-Protokolle"
        },
        "columns": {
          "mappingMode": "autoMapInputData"
        },
        "options": {}
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets OAuth2"
        }
      },
      "notes": "Archiviert jedes Meeting-Protokoll mit Zusammenfassung, To-Dos (als JSON), Entscheidungen und Metadaten in Google Sheets."
    },
    {
      "id": "gmail-send",
      "name": "Gmail: Protokoll an Teilnehmer",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.2,
      "position": [2500, 50],
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.email_empfaenger }}",
        "subject": "={{ $json.email_betreff }}",
        "emailType": "html",
        "message": "={{ $json.email_html }}",
        "options": {}
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "",
          "name": "Gmail OAuth2"
        }
      },
      "notes": "Sendet das formatierte Protokoll per Gmail an alle Teilnehmer. Voraussetzung: Teilnehmer-E-Mails wurden im Webhook mitgeliefert."
    },
    {
      "id": "smtp-send",
      "name": "SMTP: Protokoll an Teilnehmer",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [2500, 200],
      "parameters": {
        "resource": "email",
        "operation": "send",
        "fromEmail": "meetings@deine-firma.de",
        "toEmail": "={{ $json.email_empfaenger }}",
        "subject": "={{ $json.email_betreff }}",
        "emailFormat": "html",
        "html": "={{ $json.email_html }}",
        "options": {}
      },
      "credentials": {
        "smtp": {
          "id": "",
          "name": "SMTP Firmen-Mail"
        }
      },
      "disabled": true,
      "notes": "ALTERNATIVE zu Gmail: Aktiviere diesen Node und deaktiviere Gmail, wenn du lieber über deinen eigenen SMTP-Server senden willst. Passe die Absender-Adresse an."
    },
    {
      "id": "slack-notify",
      "name": "Slack: Zusammenfassung posten",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [2500, 350],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": {
          "__rl": true,
          "mode": "name",
          "value": "#meetings"
        },
        "text": "=:clipboard: *Meeting-Protokoll: {{ $json.meeting_title }}*\n:calendar: {{ $json.meeting_date }}\n\n*Zusammenfassung:*\n{{ $json.zusammenfassung }}\n\n:dart: *To-Dos ({{ $json.todos_anzahl }}):*\n{{ $json.todos_text }}\n\n{{ $json.naechstes_meeting ? ':date: Nächstes Meeting: ' + $json.naechstes_meeting : '' }}\n{{ $json.recording_url ? ':movie_camera: <' + $json.recording_url + '|Recording ansehen>' : '' }}\n\n:email: Protokoll wurde per E-Mail an alle Teilnehmer gesendet.",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack API"
        }
      },
      "notes": "Postet eine Zusammenfassung mit To-Do-Liste in den #meetings Slack-Channel. Kanal kann angepasst werden."
    },
    {
      "id": "error-notify",
      "name": "Fehler melden",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [2250, 500],
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": {
          "__rl": true,
          "mode": "name",
          "value": "#meetings"
        },
        "text": "=:warning: *Meeting-Analyse fehlgeschlagen*\n\nMeeting: {{ $json.meeting_title || 'Unbekannt' }}\nFehler: {{ $json.message }}\n\nBitte manuell prüfen.",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "id": "",
          "name": "Slack API"
        }
      }
    }
  ],
  "connections": {
    "Meeting-Recording empfangen": {
      "main": [
        [
          {
            "node": "Transkript oder Audio?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transkript oder Audio?": {
      "main": [
        [
          {
            "node": "Vorhandenes Transkript nutzen",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Audio transkribieren (OpenAI Whisper)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vorhandenes Transkript nutzen": {
      "main": [
        [
          {
            "node": "Pfade zusammenführen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Audio transkribieren (OpenAI Whisper)": {
      "main": [
        [
          {
            "node": "Whisper-Ergebnis aufbereiten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Whisper-Ergebnis aufbereiten": {
      "main": [
        [
          {
            "node": "Pfade zusammenführen",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Pfade zusammenführen": {
      "main": [
        [
          {
            "node": "KI: Meeting analysieren & To-Dos erstellen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "KI: Meeting analysieren & To-Dos erstellen": {
      "main": [
        [
          {
            "node": "Ergebnis parsen & E-Mail bauen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ergebnis parsen & E-Mail bauen": {
      "main": [
        [
          {
            "node": "Analyse OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyse OK?": {
      "main": [
        [
          {
            "node": "Protokoll-Archiv (Google Sheets)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fehler melden",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Protokoll-Archiv (Google Sheets)": {
      "main": [
        [
          {
            "node": "Gmail: Protokoll an Teilnehmer",
            "type": "main",
            "index": 0
          },
          {
            "node": "SMTP: Protokoll an Teilnehmer",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack: Zusammenfassung posten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": [
    {
      "name": "Meetings"
    },
    {
      "name": "Produktivität"
    },
    {
      "name": "Automatisierung"
    }
  ]
}
