Fathom Résumé de réunion et Agent d'action AI
Ceci est uncontenant 18 nœuds.Utilise principalement des nœuds comme If, Set, Code, Webhook, GoogleDrive. Convertir des transcriptions de réunions Fathom en résumés Google Document formatés avec l'IA Gemini
- •Point de terminaison HTTP Webhook (généré automatiquement par n8n)
- •Informations d'identification Google Drive API
- •Peut nécessiter les informations d'identification d'authentification de l'API cible
- •Clé API Google Gemini
Nœuds utilisés (18)
Catégorie
{
"id": "AJch0Bi1L5u67nRO",
"meta": {
"instanceId": "90dd23d886c9cb675f452d0fb004af6ee783e4e974ef4384cbfad1854c68a875",
"templateCredsSetupCompleted": true
},
"name": "Fathom Meeting Summary & Actions AI Agent",
"tags": [],
"nodes": [
{
"id": "a362776f-d572-4983-9dc6-a161ddd30392",
"name": "Modèle de chat Google Gemini",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
96,
16
],
"parameters": {
"options": {},
"modelName": "models/gemini-2.5-pro"
},
"credentials": {
"googlePalmApi": {
"id": "etClcv7ej0yswPTF",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1
},
{
"id": "73dc3249-bc5d-4676-95e8-bcd4b2fea8e9",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
272,
16
],
"parameters": {
"jsonSchemaExample": "{\n \"Meeting Title\": \"Test call\",\n \"Recording URL\": \"https://fathom.video/share/745cvtcxSXKb7YyfDMiyzMChZ3AsoGad\",\n \"Scheduled Date/Time\": \"30-01-2025, 12:15 to 12:30\",\n \"Recording Date/Time\": \"30-01-2025, 12:16 to 12:16\",\n \"Attendees\": \"John Doe <johndoe@domain.com> (Attendee)\",\n \"Executive Summary\": \"Single-speaker intro explaining that Fathom auto-records meetings, captures key moments and action items, and allows manual highlights. No concrete decisions or actions were stated.\",\n \"Key Points\": \"Fathom auto-records and captures important moments and action items.\\nPost-meeting summaries are delivered quickly.\\nManual highlight button available for special moments.\",\n \"Action Items\": \"Send John 2-3 specific YouTube video links.\\nGet automation engineer to review Sheets integration Mon-Tue.\",\n \"Decisions\": \"Emmily to update feature on the contact page.\\nJohn will speak to Jess about product development roadmap.\",\n \"Risks/Concerns\": \"John mentioned development pace is slow in January meaning a late release date is likely.\",\n \"Open Questions\": \"What's the status of the Hubspot implementation?\\nNone identified in the transcript.\",\n \"Entities — People\": \"Emmily Bowman; John Doe\",\n \"Entities — Orgs/Places\": \"Fathom\",\n \"Entities — Numbers/Dates\": \"30 seconds\"\n}\n"
},
"typeVersion": 1.3
},
{
"id": "a4e520d6-c9bd-4060-94b4-e61f7ef38b19",
"name": "Format Key Parts",
"type": "n8n-nodes-base.code",
"position": [
-368,
-96
],
"parameters": {
"jsCode": "// Minimal formatter: merged transcript turns with per-line timestamps\n\nconst b = $json.body ?? $json ?? {};\nconst toStr = (v) => (typeof v === 'string' ? v.trim() : (v ?? '') + '');\nconst tsToSec = (ts) => {\n const m = typeof ts === 'string' && ts.match(/^(\\d{2}):(\\d{2}):(\\d{2})$/);\n if (!m) return 0;\n const [, hh, mm, ss] = m;\n return (+hh) * 3600 + (+mm) * 60 + (+ss);\n};\nconst uniqBy = (arr, keyFn) => {\n const seen = new Set();\n return arr.filter(x => {\n const key = keyFn(x);\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n};\n\n// ---- Attendees (optional) ----\nconst invitees = Array.isArray(b.calendar_invitees) ? b.calendar_invitees : [];\nconst attendeesRaw = [\n ...invitees.map(i => ({\n name: toStr(i.name) || toStr(i.matched_speaker_display_name) || 'Unknown',\n email: toStr(i.email) || null,\n role: 'Attendee',\n })),\n ...(b.recorded_by ? [{\n name: toStr(b.recorded_by.name) || 'Unknown',\n email: toStr(b.recorded_by.email) || null,\n role: 'Host',\n }] : []),\n];\nconst attendees = uniqBy(attendeesRaw, a => a.email ? `e:${a.email.toLowerCase()}` : `n:${a.name.toLowerCase()}`);\n\n// ---- Per-line → normalize + sort ----\nconst tItems = Array.isArray(b.transcript) ? b.transcript : [];\nconst lines = tItems\n .map(i => ({\n timestamp: toStr(i?.timestamp) || '00:00:00',\n seconds: tsToSec(toStr(i?.timestamp)),\n speaker: toStr(i?.speaker?.display_name) || 'Unknown',\n email: toStr(i?.speaker?.matched_calendar_invitee_email) || null,\n text: toStr(i?.text),\n }))\n .filter(x => x.text)\n .sort((a, c) => a.seconds - c.seconds)\n .map(({ seconds, ...rest }) => rest);\n\n// ---- Merge consecutive lines by the same person, but keep per-line timestamps ----\nconst norm = s => (s || '').trim().replace(/\\s+/g, ' ').toLowerCase();\nconst samePerson = (a, b) => {\n const ae = (a.email || '').toLowerCase();\n const be = (b.email || '').toLowerCase();\n if (ae && be) return ae === be;\n const an = norm(a.speaker);\n const bn = norm(b.speaker);\n if (!ae && !be && (an === 'unknown' || bn === 'unknown')) return false;\n return an === bn;\n};\n\nconst transcript_merged = [];\nfor (const line of lines) {\n const last = transcript_merged[transcript_merged.length - 1];\n const entry = { ts: line.timestamp, text: line.text.trim() };\n\n if (last && samePerson(last, line)) {\n last.lines.push(entry); // keep per-line timestamps\n last.texts.push(entry.text); // backwards-compat (array of strings)\n last.end_timestamp = line.timestamp;\n } else {\n transcript_merged.push({\n speaker: line.speaker,\n email: line.email || null,\n start_timestamp: line.timestamp,\n end_timestamp: line.timestamp,\n start_seconds: tsToSec(line.timestamp),\n lines: [entry], // <-- preferred for HTML rendering\n texts: [entry.text], // <-- legacy compatibility\n });\n }\n}\n\n// ---- Pretty text (one line per turn; keeps the start ts for the turn) ----\nconst SENTENCE_SEP = ' ';\nconst transcript_text = transcript_merged\n .map(m => `[${m.start_timestamp}] ${m.speaker}${m.email ? ` <${m.email}>` : ''}: ${m.lines.map(x => x.text).join(SENTENCE_SEP)}`)\n .join('\\n');\n\n// ---- Output ----\nreturn {\n json: {\n meeting_title: toStr(b.meeting_title) || toStr(b.title) || null,\n attendees,\n scheduled_start_time: toStr(b.scheduled_start_time) || null,\n scheduled_end_time: toStr(b.scheduled_end_time) || null,\n recording_start_time: toStr(b.recording_start_time) || null,\n recording_end_time: toStr(b.recording_end_time) || null,\n recording_url: toStr(b.share_url) || toStr(b.url) || null,\n transcript_merged, // now includes lines[{ts,text}] + texts[]\n transcript_text, // still available if you need it\n // summary_markdown: toStr(b?.default_summary?.markdown_formatted) || null, // optional\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "a0653858-8a8c-4cc2-a5e1-c67a0a6d192d",
"name": "Définir Fields",
"type": "n8n-nodes-base.set",
"position": [
432,
-112
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "180a6cda-50cc-4533-91a8-0736d1a9cd1f",
"name": "output['Meeting Title']",
"type": "string",
"value": "={{ $json.output[\"Meeting Title\"] + \" - \" + $json.output[\"Scheduled Date/Time\"].replace(/:/g, \"-\") }}"
},
{
"id": "957ef493-cd8b-4359-bb67-25206ac2a6f1",
"name": "output['Recording URL']",
"type": "string",
"value": "={{ $json.output['Recording URL'] }}"
},
{
"id": "38301e62-074a-4206-824a-64b37da4dd91",
"name": "output['Scheduled Time']",
"type": "string",
"value": "={{ $json.output['Scheduled Date/Time'] }}"
},
{
"id": "9b4fa931-0fce-4882-91b2-0af0523005ae",
"name": "output['Recording Time']",
"type": "string",
"value": "={{ $json.output['Recording Date/Time'] }}"
},
{
"id": "38605fc1-6c2d-4d4a-b2a5-597197dc3bd1",
"name": "output.Attendees",
"type": "string",
"value": "={{ $json.output.Attendees }}"
},
{
"id": "bd1f7948-0e94-4834-9066-bb92901a3867",
"name": "output['Executive Summary']",
"type": "string",
"value": "={{ $json.output['Executive Summary'] }}"
},
{
"id": "2757b576-8558-4089-9c6b-9782a42c32ca",
"name": "output['Key Points']",
"type": "string",
"value": "={{ $json.output['Key Points'] }}"
},
{
"id": "38b5bebe-b7ad-4e03-bbb9-64818b18cde6",
"name": "output['Action Items']",
"type": "string",
"value": "={{ $json.output['Action Items'] }}"
},
{
"id": "d07cb155-8799-4110-a244-f052471614e5",
"name": "output.Decisions",
"type": "string",
"value": "={{ $json.output.Decisions }}"
},
{
"id": "7a63e1ae-209e-4d6b-a042-3e59b7f1fe1d",
"name": "output['Risks/Concerns']",
"type": "string",
"value": "={{ $json.output['Risks/Concerns'] }}"
},
{
"id": "4ea08434-1d26-4a05-95d8-12bf2270a4a3",
"name": "output['Open Questions']",
"type": "string",
"value": "={{ $json.output['Open Questions'] }}"
},
{
"id": "20424379-7edc-4f43-b304-814dff3e713e",
"name": "output['Entities — People']",
"type": "string",
"value": "={{ $json.output['Entities — People'] }}"
},
{
"id": "d7eb163b-2de3-4dc7-bc8a-70b2bbef2ea1",
"name": "output['Entities — Orgs/Places']",
"type": "string",
"value": "={{ $json.output['Entities — Orgs/Places'] }}"
},
{
"id": "a8b13e21-867c-49c1-a920-237317be64d9",
"name": "output['Entities — Numbers/Dates']",
"type": "string",
"value": "={{ $json.output['Entities — Numbers/Dates'] }}"
},
{
"id": "e7438bd6-dec3-4162-a66a-fe7fa5e17056",
"name": "transcript_merged",
"type": "array",
"value": "={{ $('Format Key Parts').item.json.transcript_merged }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "431be23c-a097-4cea-a77d-ee9e9a574ab5",
"name": "Create as HTML",
"type": "n8n-nodes-base.code",
"position": [
704,
-112
],
"parameters": {
"jsCode": "// --- helpers ---\nconst esc = (s='') => s.replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]));\nconst nl2li = s => s.split('\\n').map(x=>`<li>${esc(x.replace(/^-+\\s?/, ''))}</li>`).join('');\nconst chunk = (arr, n=3) => arr.reduce((a,_,i)=>(i%n?a[a.length-1].push(arr[i]):a.push([arr[i]]),a),[]);\n\n// --- inputs ---\nconst o = $json.output; // summary sections\nconst turns = ($node[\"Set Fields\"]?.json?.transcript_merged) || $json.transcript_merged || [];\n\n// --- transcript (speaker change + every 3 sentences; timestamp from first sentence in chunk) ---\nlet transcriptHtml = '—';\nif (Array.isArray(turns) && turns.length) {\n transcriptHtml = turns.map(t => {\n const parts = chunk((t.lines || []).filter(x => x && x.text), 3);\n return parts.map(group => {\n const stamp = group[0]?.ts || t.start_timestamp;\n const text = group.map(x => (x.text || '').trim()).join(' ');\n // email intentionally omitted\n return `<strong>[${esc(stamp)}] ${esc(t.speaker)}:</strong> ${esc(text)}<br>`;\n }).join('');\n }).join('\\n');\n}\n\n// --- HTML (H2/H3/H4; compact, readable) ---\nconst html = `\n <h2>${esc(o[\"Meeting Title\"])}</h2><br>\n\n <h3>Scheduled Time</h3><p>${esc(o[\"Scheduled Time\"])}</p><br>\n <h3>Recording Time</h3><p>${esc(o[\"Recording Time\"])}</p><br>\n <h3>Recording URL</h3><p>${o[\"Recording URL\"] === \"None identified in the transcript\" ? \"—\" : `<a href=\"${esc(o[\"Recording URL\"])}\">${esc(o[\"Recording URL\"])}</a>`}</p><br>\n\n <h3>Attendees</h3><ul>${nl2li(o[\"Attendees\"])}</ul><br>\n <h3>Executive Summary</h3><p>${esc(o[\"Executive Summary\"])}</p><br>\n <h3>Key Points</h3><ul>${nl2li(o[\"Key Points\"])}</ul><br>\n <h3>Action Items</h3><ul>${nl2li(o[\"Action Items\"])}</ul><br>\n <h3>Decisions</h3><ul>${nl2li(o[\"Decisions\"])}</ul><br>\n <h3>Risks/Concerns</h3><ul>${nl2li(o[\"Risks/Concerns\"])}</ul><br>\n <h3>Open Questions</h3><ul>${nl2li(o[\"Open Questions\"])}</ul><br>\n\n <h3>Entities</h3>\n <h4>People</h4><ul>${nl2li(o[\"Entities — People\"])}</ul>\n <h4>Orgs/Places</h4><ul>${nl2li(o[\"Entities — Orgs/Places\"])}</ul>\n <h4>Numbers/Dates</h4><ul>${nl2li(o[\"Entities — Numbers/Dates\"])}</ul><br>\n\n <br><br><hr><br>\n <h3>Full Transcript</h3>\n ${transcriptHtml}\n`.trim();\n\n// -> Binary for Drive Upload (Convert to Google Doc = ON)\nconst filename = `${o[\"Meeting Title\"]} - Meeting Notes.html`;\nconst buf = Buffer.from(html, 'utf8');\nconst data = await this.helpers.prepareBinaryData(buf, filename, 'text/html');\nreturn [{ json: { filename }, binary: { data } }];\n"
},
"typeVersion": 2
},
{
"id": "236d20f7-0deb-48bd-b255-b7d72595958a",
"name": "Convert to Google Doc",
"type": "n8n-nodes-base.httpRequest",
"position": [
1120,
-112
],
"parameters": {
"url": "=https://www.googleapis.com/drive/v3/files/{{ $json.id }}/copy",
"method": "POST",
"options": {},
"jsonBody": "={\n \"name\": \"{{ $json.name.replace('.html', '') }}\",\n \"mimeType\": \"application/vnd.google-apps.document\"\n }",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "googleDriveOAuth2Api"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "IjoB5flCkLlcfjdH",
"name": "Google Drive account"
}
},
"typeVersion": 4.2
},
{
"id": "4c05b6b7-76ee-4747-8f5c-1fc8f679407b",
"name": "Téléverser fichier as HTML",
"type": "n8n-nodes-base.googleDrive",
"position": [
912,
-112
],
"parameters": {
"name": "={{ $json.filename }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "list",
"value": "root",
"cachedResultUrl": "https://drive.google.com/drive",
"cachedResultName": "/ (Root folder)"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "IjoB5flCkLlcfjdH",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "0387aca5-1023-4606-9983-5e32801bf771",
"name": "Delete HTML File",
"type": "n8n-nodes-base.googleDrive",
"position": [
1344,
16
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Upload File as HTML').item.json.id }}"
},
"options": {
"deletePermanently": true
},
"operation": "deleteFile"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "IjoB5flCkLlcfjdH",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "744b6d13-5fb4-4da2-8087-1d20a44bf548",
"name": "AI Meeting Analysis",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
96,
-144
],
"parameters": {
"text": "=For the meeting transcript provided at the bottom, please follow the below template to write your reviewing comments about the meeting.\nNote that if there are sections below that you cannot identify in the transcript, then simply mention \"None identified in the transcript\"\n\n---\n\nMeeting:\nHeader line, nicely formatted and using the information exactly as provided.\nUse the official meeting title here (if provided): '{{ $json.meeting_title }}'\nOr, if there is no meeting title provided, create a title based on your analysis of the raw transcript provided at the end.\n\nRecording URL:\n{{ $json.recording_url }}\n\nScheduled Start Date/Time and End Time:\nFormatted as 'DD-MM-YYYY - HH:MM to HH:MM': {{ $json.scheduled_start_time }} - {{ $json.scheduled_end_time }} \n\nRecording Start Date/Time and End Time:\nFormatted as 'DD-MM-YYYY - HH:MM to HH:MM': {{ $json.recording_start_time }} - {{ $json.recording_end_time }}\n\n---\n\nAttendees:\n- names and email addresses, where available\nAttendee official list:{{ JSON.stringify($json.attendees) }}\nAlso check the transcript to identify additional speakers where possible\n\n\nExecutive Summary:\n- {1–5 sentences, no fluff. What’s the meeting about, key outcome(s), and immediate next step(s)?} \n\nKey Points:\n- {Concise point}.\n- {Another concise point}.\n(8 bullets max, but less is fine if more effective)\n\nAction Items:\n- List of each action item with any mentioned Owner, Due Date, Priority, and Source\n\nKey Decisions Made:\n- Decision:** {What was decided}. (By {who}, if stated) \n(6 bullets max, but less is fine if more effective)\n\n\nKey Risks / Concerns:\n- {Risk/issue}: {Context or impact in 1 line}\n(6 items maximum, but less is fine if more effective)\n\nOpen Questions:\n- {Question asked or unresolved point}\n- {Any missing info needed to proceed}\n(6 items maximum, but less is fine if more effective)\n\nEntities & References (quick extract):\n- People: {names} \n- Organizations/Places: {entities} \n- Numbers & Dates Mentioned (with brief context of why any numbers and dates are provided): {normalize money, dates, durations if present}\n\n\n**For Key Points, Action Items, Decisions, Risks/Concerns, Open Questions: return a single string. If items exist, output them as newline-separated lines (no bullets/numbers). If none, return the exact string “None identified in the transcript”.**\n\n---\n\n*** HERE IS THE TRANSCRIPT: ***\n\n{{ JSON.stringify($json.transcript_merged) }}",
"batching": {},
"messages": {
"messageValues": [
{
"message": "You are an expert meeting analyst. Produce a concise, executive-quality report from the transcript. Be precise, non-fluffy, and focus on important takeaways and outcomes from the meeting."
}
]
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "dc09aed6-a43b-470a-9463-72699cf38d4b",
"name": "Transcript Present?",
"type": "n8n-nodes-base.if",
"position": [
-176,
-96
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "validation-check-1",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ JSON.stringify($json.transcript_merged) }}",
"rightValue": ""
},
{
"id": "validation-check-2",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $json.transcript_merged.length }}",
"rightValue": 3
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a7e3e5c0-64be-462c-a86c-2325e783f693",
"name": "Note adhésive",
"type": "n8n-nodes-base.stickyNote",
"position": [
-624,
-272
],
"parameters": {
"width": 640,
"height": 464,
"content": "## Capture Meeting and Validate\nGet all Fathom meeting data, extract + format relevant contents, and validate there's transcript data before triggering the Gemini API request."
},
"typeVersion": 1
},
{
"id": "ac4a64c4-6475-4b1e-a0bb-548a253350a0",
"name": "Note adhésive1",
"type": "n8n-nodes-base.stickyNote",
"position": [
32,
-272
],
"parameters": {
"color": 6,
"width": 576,
"height": 464,
"content": "## Perform Meeting Analysis (AI)\nConduct full analysis of the meeting transcript and associated data, and create the meeting notes (summary, actions, etc.)."
},
"typeVersion": 1
},
{
"id": "9a58821a-8ff7-46cd-81e9-4407b69f04d3",
"name": "Note adhésive2",
"type": "n8n-nodes-base.stickyNote",
"position": [
624,
-272
],
"parameters": {
"color": 3,
"width": 944,
"height": 464,
"content": "## Convert to HTML Output and Upload to Google Drive\nGet all the meeting data + the newly generated meeting notes (from Gemini), create in HTML format (with correct MIME type for HTML rendering), create as Google Doc, and delete HTML file."
},
"typeVersion": 1
},
{
"id": "bc8793a2-268d-4379-81de-057fe26b0ce7",
"name": "Improve Page Layout",
"type": "n8n-nodes-base.httpRequest",
"position": [
1344,
-160
],
"parameters": {
"url": "=https://docs.googleapis.com/v1/documents/{{$node[\"Convert to Google Doc\"].json.id}}:batchUpdate",
"method": "POST",
"options": {},
"jsonBody": "{\n \"requests\": [\n {\n \"updateDocumentStyle\": {\n \"documentStyle\": {\n \"marginTop\": {\"magnitude\": 36, \"unit\": \"PT\"},\n \"marginBottom\": {\"magnitude\": 36, \"unit\": \"PT\"},\n \"marginLeft\": {\"magnitude\": 36, \"unit\": \"PT\"},\n \"marginRight\": {\"magnitude\": 36, \"unit\": \"PT\"}\n },\n \"fields\": \"marginTop,marginBottom,marginLeft,marginRight\"\n }\n }\n ]\n}\n",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "googleDriveOAuth2Api"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "IjoB5flCkLlcfjdH",
"name": "Google Drive account"
}
},
"typeVersion": 4.2
},
{
"id": "76eeb8a0-d87a-442c-8cc9-924b7ee28b59",
"name": "Note adhésive3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1472,
-736
],
"parameters": {
"color": 7,
"width": 816,
"height": 1168,
"content": "# **Fathom Meeting Summary & Actions AI Agent**\n *Transcript → Analysis → Formatted Doc, automatically*\n\nThis workflow automatically converts Fathom meeting transcripts into beautifully formatted Google Docs with AI-generated summaries, key points, decisions, and action items.\n\n **Good to know**\n - Works fully with Fathom free account\n - Webhook responds immediately to prevent Fathom timeout and duplicate triggers\n - Validates transcript quality (3+ conversation turns) before AI processing to save costs\n - Uses Google Gemini API (generous free tier and rate limits, typically enough to avoid paying for API requests, but check latest pricing at [Google AI Pricing](https://ai.google.dev/pricing))\n - Creates temporary HTML file that's auto-deleted after conversion\n\n ## Who's it for\n Individuals or teams using Fathom for meetings who want more control and flexibility with their AI meeting analysis and storage independently of Fathom, as well as automatic, formatted documentation without manual note-taking. Perfect for recurring syncs, client meetings, or interview debriefs.\n\n ## How it works\n 1. Fathom webhook triggers when meeting ends and sends transcript data\n 2. Validates transcript has meaningful conversation (3+ turns)\n 3. Google Gemini AI analyzes transcript and generates structured summary (key points, decisions, actions, next steps)\n 4. Creates formatted HTML with proper styling\n 5. Uploads to Google Drive and converts to native Google Doc\n 6. Reduces page margins for readability and deletes temporary HTML file\n\n ## Requirements\n - Fathom account with API webhook access (available on free tier)\n - Google Drive account (OAuth2)\n - Google Docs account (OAuth2)\n - Google Gemini API key ([Get free key here](https://makersuite.google.com/app/apikey))\n\n ## How to set up\n 1. Add credentials: Google Drive OAuth2, Google Docs OAuth2, Google Gemini API\n 2. Copy the webhook URL from the Get Fathom Meeting webhook node (Test URL first, change to Production URL when ready)\n 3. In Fathom: Settings → API Access → Add → Add webhook URL and select all events including \"Transcript\"\n 4. Test with a short meeting, verify Google Doc appears in Drive\n 5. Activate workflow\n\n ## Customizing this workflow\n - **Change save location**: Edit \"Upload File as HTML\" node → update \"Parent Folder\"\n - **Modify AI output**: Edit \"AI Meeting Analysis\" node → customize the prompt to add/remove sections (e.g., risks, follow-ups, sentiment, etc)\n - **Adjust document margins**: Edit \"Reduce Page Margins\" node → change margin pixel values\n - **Add notifications**: E.g. add Slack/Email node after \"Convert to Google Doc\" to notify team when summary is ready"
},
"typeVersion": 1
},
{
"id": "ca149d7b-3a90-4882-bc9d-2dbddf83872c",
"name": "Get Fathom Meeting",
"type": "n8n-nodes-base.webhook",
"position": [
-560,
-96
],
"webhookId": "2fab6c8f-ade4-49ba-b160-7cf6aa11cb15",
"parameters": {
"path": "2fab6c8f-ade4-49ba-b160-7cf6aa11cb15",
"options": {
"rawBody": true,
"binaryPropertyName": "data"
},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "705471d5-b542-49f6-9342-660884907ea4",
"name": "Note adhésive7",
"type": "n8n-nodes-base.stickyNote",
"position": [
224,
-496
],
"parameters": {
"color": 7,
"width": 640,
"height": 192,
"content": " ## **Quick Troubleshooting**\n * **\"Transcript Present?\" fails**: Fathom must send `transcript_merged` with 3+ conversation turns (i.e. only send to Gemini for analysis if there's a genuine transcript)\n * **HTML as plain text**: Check \"Convert to Google Doc\" uses POST to `/copy` endpoint\n * **401/403 errors**: Re-authorize Google credentials\n * **Inadequate meeting notes**: Edit prompts in \"AI Meeting Analysis\" node"
},
"typeVersion": 1
},
{
"id": "7fd24ad7-f811-42ee-9ca9-fb8490cdf8eb",
"name": "Note adhésive4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-624,
-640
],
"parameters": {
"color": 7,
"width": 816,
"height": 336,
"content": "## Sample File and Storage Output\n- [Google Doc meeting notes - sample](https://docs.google.com/document/d/1tCC90dIpgb8NtuOJ_jSTCPn4MxORB-XcvwdeljYzC9w/edit?usp=drive_link)\n- Google Drive sample folder output:\n"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "NrO9p0iGYkywEkTP",
"executionOrder": "v1"
},
"versionId": "f8839c0b-a795-4745-bca5-dab6a00a5977",
"connections": {
"Set Fields": {
"main": [
[
{
"node": "431be23c-a097-4cea-a77d-ee9e9a574ab5",
"type": "main",
"index": 0
}
]
]
},
"431be23c-a097-4cea-a77d-ee9e9a574ab5": {
"main": [
[
{
"node": "Upload File as HTML",
"type": "main",
"index": 0
}
]
]
},
"a4e520d6-c9bd-4060-94b4-e61f7ef38b19": {
"main": [
[
{
"node": "dc09aed6-a43b-470a-9463-72699cf38d4b",
"type": "main",
"index": 0
}
]
]
},
"ca149d7b-3a90-4882-bc9d-2dbddf83872c": {
"main": [
[
{
"node": "a4e520d6-c9bd-4060-94b4-e61f7ef38b19",
"type": "main",
"index": 0
}
]
]
},
"744b6d13-5fb4-4da2-8087-1d20a44bf548": {
"main": [
[
{
"node": "Set Fields",
"type": "main",
"index": 0
}
]
]
},
"dc09aed6-a43b-470a-9463-72699cf38d4b": {
"main": [
[
{
"node": "744b6d13-5fb4-4da2-8087-1d20a44bf548",
"type": "main",
"index": 0
}
]
]
},
"Upload File as HTML": {
"main": [
[
{
"node": "236d20f7-0deb-48bd-b255-b7d72595958a",
"type": "main",
"index": 0
}
]
]
},
"236d20f7-0deb-48bd-b255-b7d72595958a": {
"main": [
[
{
"node": "0387aca5-1023-4606-9983-5e32801bf771",
"type": "main",
"index": 0
},
{
"node": "bc8793a2-268d-4379-81de-057fe26b0ce7",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "744b6d13-5fb4-4da2-8087-1d20a44bf548",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"73dc3249-bc5d-4676-95e8-bcd4b2fea8e9": {
"ai_outputParser": [
[
{
"node": "744b6d13-5fb4-4da2-8087-1d20a44bf548",
"type": "ai_outputParser",
"index": 0
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Dean Pike
@deanjpVersatile problem-solver building scalable, AI-driven systems that replace manual, error-prone operations for high-growth companies. Specializing in operational strategy, project delivery, and practical AI and workflow automation solutions that free leaders to focus on strategic priorities while increasing margins without additional hires.
Partager ce workflow