This workflow corresponds to n8n.io template #15818 — we link there as the canonical source.
This workflow follows the Agent → Gmail recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "e041e70d-9d8d-4a2a-8faf-42dca7ea959a",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-7376,
-592
],
"parameters": {
"color": 4,
"width": 732,
"height": 1428,
"content": "## Zoom AI Meeting Assistant \u2014 Claude 3.7 Sonnet + ClickUp + Google Calendar + Slack + Sheets\n\nFor teams who use Zoom for meetings and want to automatically process every recorded meeting \u2014 transcript download, AI summary, participant emails, ClickUp tasks, calendar follow-up, Slack notification, and Sheets log \u2014 without manual effort after each call. Every day at 9AM (or on manual trigger) this workflow fetches all Zoom meetings from the last 24 hours. For each meeting it downloads the VTT transcript file, parses it into clean readable text, fetches all participants from the Zoom API, and generates a structured AI summary using Claude 3.7 Sonnet with Think Tool. A Code node formats the summary into an HTML email, deduplicates participant emails, and splits into one item per participant. Gmail sends the summary to every participant individually. A second Code node extracts action items from the summary and creates one ClickUp task per item via HTTP API. A third Code node extracts follow-up meeting details and creates a Google Calendar event if a next meeting was mentioned. Slack sends a team notification with a status summary. Google Sheets logs every meeting with task count and follow-up status.\n\n## How it works\n- **1. Schedule \u2014 Every Day 9AM** and **2. Manual Trigger** both connect to node 3 \u2014 run automatically or on demand\n- **3. Zoom \u2014 Get All Meetings** fetches all scheduled meetings via Zoom OAuth2\n- **4. Filter \u2014 Last 24 Hours Only** keeps only meetings from the past 24 hours\n- **5. Zoom \u2014 Get Recording Data** fetches recording files \u2014 error output goes to **5b. Stop** if no recording exists\n- **6. Code \u2014 Extract Transcript URL** finds the TRANSCRIPT file type and extracts its download URL\n- **7. Zoom \u2014 Download Transcript File** downloads the VTT transcript as text\n- **8. Code \u2014 Parse Transcript Text** parses the WEBVTT format into clean readable lines\n- **9. Zoom \u2014 Get All Participants** fetches participant names and emails from the past meeting API\n- **10. Claude 3.7 \u2014 Generate Meeting Summary** uses Claude 3.7 Sonnet + Think Tool to write a structured 6-section summary\n- **11. Code \u2014 Format Email + Split Participants** builds the HTML email, deduplicates participants, returns one item per email address\n- **12. Gmail \u2014 Send to Each Participant** sends the summary email to each participant individually\n- **13. Code \u2014 Extract Action Items** parses the Action Items section from the summary\n- **14. IF \u2014 Has Action Items?** \u2014 TRUE: splits tasks and creates ClickUp tasks via HTTP \u2192 then proceeds to follow-up | FALSE: goes directly to follow-up\n- **17. Code \u2014 Extract Follow-Up Info** parses the Follow-up Meeting section\n- **18. IF \u2014 Has Follow-Up?** \u2014 TRUE: creates Google Calendar event | FALSE: goes directly to Slack\n- **20. Slack \u2014 Send Team Notification** sends a status summary with checkmarks for each completed action\n- **21. Google Sheets \u2014 Log Meeting** appends one row with meeting metadata and processing status\n\n## Set up steps\n1. **Zoom OAuth2** \u2014 create an OAuth app at marketplace.zoom.us with scopes meeting:read and recording:read. Connect in n8n and use in nodes 3, 5, 7, 9\n2. **Anthropic API** \u2014 get key from console.anthropic.com. Connect in **Anthropic \u2014 Claude 3.7 Sonnet Model**. Zoom Pro or higher with Cloud Recording and Auto-Transcription enabled is required\n3. **ClickUp** \u2014 get API token from ClickUp Settings \u2192 Apps \u2192 API Token. Replace `YOUR_CLICKUP_API_TOKEN` and `YOUR_CLICKUP_LIST_ID` in node 16\n4. **Google Calendar** \u2014 connect OAuth2 credential in node 19. Replace `YOUR_CALENDAR_ID` (usually your email address)\n5. **Gmail** \u2014 connect OAuth2 credential in node 12\n6. **Slack** \u2014 connect OAuth2 credential in node 20. Replace `YOUR_SLACK_CHANNEL_ID`\n7. **Google Sheets** \u2014 connect OAuth2 credential in node 21. Replace `YOUR_GOOGLE_SHEET_ID`. Create tab Meeting Log with columns: Date, Meeting Title, Meeting ID, Duration (min), Participants, Summary Sent, Tasks Created, Follow-up Created, Processed At"
},
"typeVersion": 1
},
{
"id": "3e4eafda-6163-43e1-9fed-23d1b1d0c791",
"name": "Section \u2014 Dual Trigger and Zoom Meeting Fetch",
"type": "n8n-nodes-base.stickyNote",
"position": [
-6576,
-240
],
"parameters": {
"color": 5,
"width": 596,
"height": 452,
"content": "## Dual Trigger and Zoom Meeting Fetch\nSchedule fires at 9AM daily. Manual trigger for on-demand runs. Both connect to the same Zoom node which fetches all scheduled meetings."
},
"typeVersion": 1
},
{
"id": "e40e80b9-11c5-4938-80de-fc167c4f7d66",
"name": "Section \u2014 24-Hour Filter, Recording Fetch, Transcript Download, and Participant Lookup",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5920,
-256
],
"parameters": {
"color": 6,
"width": 1524,
"height": 580,
"content": "## 24-Hour Filter, Recording Fetch, Transcript Download, and Participant Lookup\nFilters meetings to last 24 hours only. Fetches recording files \u2014 error branch stops gracefully if no recording exists. Extracts VTT transcript URL, downloads and parses it. Fetches all participant names and emails."
},
"typeVersion": 1
},
{
"id": "87f2fde7-974f-49dc-a892-5bc4b6ce6134",
"name": "Section \u2014 Claude 3.7 Sonnet AI Summary and Email Format",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4288,
-288
],
"parameters": {
"color": 6,
"width": 644,
"height": 804,
"content": "## Claude 3.7 Sonnet AI Summary and Email Format\nClaude 3.7 Sonnet with Think Tool generates a 6-section structured summary. Code node builds HTML email, deduplicates participant emails, and splits into one item per participant for individual sending."
},
"typeVersion": 1
},
{
"id": "772a98b6-5b9e-4b7f-9ecb-18dfc28cca6b",
"name": "Section \u2014 Gmail Send to Each Participant",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3536,
-208
],
"parameters": {
"color": 6,
"width": 436,
"height": 420,
"content": "## Gmail Send to Each Participant\nSends the HTML summary email to each unique participant email address individually."
},
"typeVersion": 1
},
{
"id": "96c042a1-e448-4591-8a34-ca9e0e567b41",
"name": "Section \u2014 Action Items, ClickUp Tasks, Follow-Up Calendar, Slack Notification, and Sheets Log",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3056,
-288
],
"parameters": {
"color": 4,
"width": 1652,
"height": 788,
"content": "## Action Items, ClickUp Tasks, Follow-Up Calendar, Slack Notification, and Sheets Log\nExtracts action items and creates ClickUp tasks (TRUE branch \u2014 node 15 at top). Extracts follow-up info and creates Google Calendar event if mentioned (TRUE branch). Slack sends status summary. Sheets logs everything. Node 21 (Sheets) is below the main flow at y=992."
},
"typeVersion": 1
},
{
"id": "c03afb36-365f-4358-be3a-77263e698c52",
"name": "1. Schedule \u2014 Every Day 9AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-6528,
-144
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "dbea2573-6e8b-4ee2-b664-59374a148dda",
"name": "2. Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-6528,
16
],
"parameters": {},
"typeVersion": 1
},
{
"id": "0a848c6e-6e18-4e28-abe3-7804fb5c6628",
"name": "3. Zoom \u2014 Get All Meetings",
"type": "n8n-nodes-base.zoom",
"position": [
-6208,
-64
],
"parameters": {
"filters": {
"type": "scheduled"
},
"operation": "getAll",
"returnAll": true,
"authentication": "oAuth2"
},
"typeVersion": 1
},
{
"id": "5c0ad34e-d04b-4e3c-96cc-55dc088cee58",
"name": "4. Filter \u2014 Last 24 Hours Only",
"type": "n8n-nodes-base.filter",
"position": [
-5872,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "after-check",
"operator": {
"type": "dateTime",
"operation": "afterOrEquals"
},
"leftValue": "={{ $json.start_time }}",
"rightValue": "={{ $now.minus({ hours: 24 }).toISO() }}"
},
{
"id": "before-check",
"operator": {
"type": "dateTime",
"operation": "beforeOrEquals"
},
"leftValue": "={{ $json.start_time }}",
"rightValue": "={{ $now.toISO() }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f61bccc8-83b4-48ff-b208-b4d3c2845c19",
"name": "5. Zoom \u2014 Get Recording Data",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"position": [
-5632,
-64
],
"parameters": {
"url": "=https://api.zoom.us/v2/meetings/{{ $json.id }}/recordings",
"options": {},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "zoomOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "172ce4af-2014-4b46-b6d2-e3874b88a231",
"name": "5b. Stop \u2014 No Recording Available",
"type": "n8n-nodes-base.stopAndError",
"position": [
-5632,
160
],
"parameters": {
"errorMessage": "No recording or transcript found for this Zoom meeting. Make sure Cloud Recording and Auto-Transcription are enabled in your Zoom settings."
},
"typeVersion": 1
},
{
"id": "dbec5ff8-cbb6-4033-913f-ce78fbaa9dc2",
"name": "6. Code \u2014 Extract Transcript URL",
"type": "n8n-nodes-base.code",
"position": [
-5392,
-64
],
"parameters": {
"jsCode": "// Extract transcript download URL from recording files\nconst recordingFiles = $json.recording_files || [];\n\nconst transcriptFile = recordingFiles.find(f => f.file_type === 'TRANSCRIPT');\n\nif (!transcriptFile || !transcriptFile.download_url) {\n throw new Error('No TRANSCRIPT file found in this meeting recording. Make sure Auto-Transcription is enabled in Zoom settings.');\n}\n\nconst meetingId = $json.id || $json.uuid;\nconst meetingTopic = $json.topic || 'Untitled Meeting';\nconst meetingStartTime = $json.start_time || new Date().toISOString();\nconst meetingDuration = $json.duration || 0;\n\nreturn [{\n json: {\n transcriptUrl: transcriptFile.download_url,\n meetingId,\n meetingTopic,\n meetingStartTime,\n meetingDuration,\n formattedDate: new Date(meetingStartTime).toLocaleString('en-US', {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n })\n }\n}];"
},
"typeVersion": 2
},
{
"id": "0044815f-b725-4bc7-b0c0-de5d839387a1",
"name": "7. Zoom \u2014 Download Transcript File",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5152,
-64
],
"parameters": {
"url": "={{ $json.transcriptUrl }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "zoomOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "35201d24-1466-40b0-a6cc-0598d315b98a",
"name": "8. Code \u2014 Parse Transcript Text",
"type": "n8n-nodes-base.code",
"position": [
-4912,
-64
],
"parameters": {
"jsCode": "// Parse Zoom VTT transcript format into clean readable text\nconst rawTranscript = $json.data || $json.body || '';\n\nif (!rawTranscript || rawTranscript.trim().length === 0) {\n throw new Error('Downloaded transcript is empty. The meeting may not have audio or transcription may still be processing.');\n}\n\nconst blocks = rawTranscript.split(/\\r?\\n\\r?\\n/);\nconst lines = [];\n\nfor (const block of blocks) {\n const blockLines = block.trim().split(/\\r?\\n/);\n if (blockLines[0] === 'WEBVTT' || blockLines.length < 2) continue;\n\n let startIndex = 0;\n if (/^\\d+$/.test(blockLines[0])) startIndex = 1;\n if (blockLines[startIndex] && blockLines[startIndex].includes('-->')) startIndex++;\n\n const textLines = blockLines.slice(startIndex).join(' ').trim();\n if (textLines) lines.push(textLines);\n}\n\nconst cleanTranscript = lines.join('\\n');\nconst meta = $('6. Code \u2014 Extract Transcript URL').item.json;\n\nreturn [{\n json: {\n transcript: cleanTranscript,\n transcriptWordCount: cleanTranscript.split(/\\s+/).length,\n meetingId: meta.meetingId,\n meetingTopic: meta.meetingTopic,\n meetingStartTime: meta.meetingStartTime,\n meetingDuration: meta.meetingDuration,\n formattedDate: meta.formattedDate\n }\n}];"
},
"typeVersion": 2
},
{
"id": "247be104-eb64-4cdb-9cde-7e092a5ae33f",
"name": "9. Zoom \u2014 Get All Participants",
"type": "n8n-nodes-base.httpRequest",
"position": [
-4528,
-64
],
"parameters": {
"url": "=https://api.zoom.us/v2/past_meetings/{{ $json.meetingId }}/participants",
"options": {},
"authentication": "predefinedCredentialType",
"nodeCredentialType": "zoomOAuth2Api"
},
"typeVersion": 4.2
},
{
"id": "4c8d20b4-4d67-4615-aa2f-259da665cab7",
"name": "10. Claude 3.7 \u2014 Generate Meeting Summary",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-4208,
-64
],
"parameters": {
"text": "=Meeting Title: {{ $('8. Code \u2014 Parse Transcript Text').item.json.meetingTopic }}\nDate: {{ $('8. Code \u2014 Parse Transcript Text').item.json.formattedDate }}\nDuration: {{ $('8. Code \u2014 Parse Transcript Text').item.json.meetingDuration }} minutes\nParticipants: {{ $json.participants.map(p => p.name + ' (' + p.user_email + ')').join(', ') }}\n\nTranscript:\n{{ $('8. Code \u2014 Parse Transcript Text').item.json.transcript }}",
"options": {
"systemMessage": "You are an expert meeting assistant. Analyze the meeting transcript and create a comprehensive, structured summary.\n\nReturn your response in this exact format with these exact section headers:\n\n## Meeting Summary\n[2-3 paragraph summary of what was discussed, decisions made, and overall meeting outcome]\n\n## Key Discussion Points\n- [Point 1]\n- [Point 2]\n- [Point 3]\n[List all major topics discussed]\n\n## Decisions Made\n- [Decision 1 with context]\n- [Decision 2 with context]\n[Write None if no decisions were made]\n\n## Action Items\n- [Task]: [Owner name] | Due: [Date if mentioned, otherwise Not specified] | Priority: [High/Medium/Low]\n[List all tasks with owners extracted from transcript]\n[Write None if no action items were mentioned]\n\n## Follow-up Meeting\nDate: [Date and time if mentioned]\nTopic: [Topic of next meeting]\nParticipants: [Who should attend]\n[Write None scheduled if no follow-up meeting was discussed]\n\n## Key Quotes\n- \"[Important quote from transcript]\" \u2014 [Speaker name]\n[Include 2-3 most important statements]\n\nBe specific, factual, and extract only what was actually said in the transcript."
},
"promptType": "define"
},
"typeVersion": 1.9
},
{
"id": "d1fc7ecc-20d5-43e9-bc52-8a9d9817661e",
"name": "Anthropic \u2014 Claude 3.7 Sonnet Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
-4208,
144
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-3-7-sonnet-20250219",
"cachedResultName": "Claude 3.7 Sonnet"
},
"options": {}
},
"typeVersion": 1.3
},
{
"id": "5b510c39-f4e5-486d-8dd6-1dbdb9be8fdd",
"name": "Think Tool",
"type": "@n8n/n8n-nodes-langchain.toolThink",
"position": [
-3936,
336
],
"parameters": {},
"typeVersion": 1
},
{
"id": "3eb616b5-dd1f-4fb8-af6e-cdd8743a3233",
"name": "11. Code \u2014 Format Email + Split Participants",
"type": "n8n-nodes-base.code",
"position": [
-3856,
-64
],
"parameters": {
"jsCode": "// Format AI summary into clean HTML email and split participants\nconst summaryRaw = $('10. Claude 3.7 \u2014 Generate Meeting Summary').item.json.output || '';\nconst participants = $('9. Zoom \u2014 Get All Participants').item.json.participants || [];\nconst meetingTopic = $('8. Code \u2014 Parse Transcript Text').item.json.meetingTopic;\nconst formattedDate = $('8. Code \u2014 Parse Transcript Text').item.json.formattedDate;\nconst meetingDuration = $('8. Code \u2014 Parse Transcript Text').item.json.meetingDuration;\nconst meetingId = $('8. Code \u2014 Parse Transcript Text').item.json.meetingId;\n\nconst formatSection = (text) => {\n return text\n .replace(/## (.+)/g, '<h2 style=\"color:#1a1a1a;font-size:18px;margin:20px 0 8px;border-bottom:2px solid #e0e0e0;padding-bottom:6px;\">$1</h2>')\n .replace(/^- (.+)/gm, '<li style=\"margin-bottom:6px;\">$1</li>')\n .replace(/(<li[^>]*>.*<\\/li>\\n?)+/g, '<ul style=\"padding-left:20px;margin:8px 0;\">$&</ul>')\n .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n .replace(/\\n\\n/g, '</p><p style=\"margin:8px 0;\">')\n .replace(/^(?!<)(.+)/gm, '<p style=\"margin:8px 0;line-height:1.6;color:#444;\">$1</p>');\n};\n\nconst htmlBody = `\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;background:#f4f4f4;\">\n <div style=\"background:#1a1a1a;padding:28px 32px;border-radius:8px 8px 0 0;\">\n <h1 style=\"color:#ffffff;font-size:22px;margin:0;\">Meeting Summary</h1>\n <p style=\"color:#aaaaaa;font-size:13px;margin:8px 0 0;\">${meetingTopic}</p>\n </div>\n <div style=\"background:#0066cc;padding:10px 32px;\">\n <p style=\"color:#ffffff;font-size:13px;margin:0;\">${formattedDate} \u2022 ${meetingDuration} minutes \u2022 ${participants.length} participants</p>\n </div>\n <div style=\"background:#ffffff;padding:24px 32px;\">\n ${formatSection(summaryRaw)}\n </div>\n <div style=\"background:#f9f9f9;padding:16px 32px;border-top:1px solid #e0e0e0;\">\n <p style=\"font-size:12px;color:#888;margin:0;\">This summary was generated automatically using Claude 3.7 Sonnet AI.</p>\n <p style=\"font-size:12px;color:#888;margin:4px 0 0;\">Meeting ID: ${meetingId}</p>\n </div>\n <div style=\"background:#1a1a1a;padding:12px 32px;border-radius:0 0 8px 8px;\">\n <p style=\"color:#888;font-size:11px;margin:0;\">Powered by n8n + Claude 3.7 Sonnet</p>\n </div>\n</body>\n</html>`;\n\nconst uniqueParticipants = [];\nconst seenEmails = new Set();\nfor (const p of participants) {\n if (p.user_email && !seenEmails.has(p.user_email)) {\n seenEmails.add(p.user_email);\n uniqueParticipants.push({ name: p.name || 'Participant', email: p.user_email });\n }\n}\n\nreturn uniqueParticipants.map(participant => ({\n json: {\n participantName: participant.name,\n participantEmail: participant.email,\n htmlEmail: htmlBody,\n emailSubject: `Meeting Summary: ${meetingTopic} \u2014 ${formattedDate}`,\n summaryRaw,\n meetingTopic,\n formattedDate,\n meetingDuration,\n meetingId,\n totalParticipants: uniqueParticipants.length\n }\n}));"
},
"typeVersion": 2
},
{
"id": "0e7f4c51-f39b-462d-b5e9-1fcb06bf2a22",
"name": "12. Gmail \u2014 Send to Each Participant",
"type": "n8n-nodes-base.gmail",
"position": [
-3344,
-64
],
"parameters": {
"sendTo": "={{ $json.participantEmail }}",
"message": "={{ $json.htmlEmail }}",
"options": {
"senderName": "Meeting Assistant"
},
"subject": "={{ $json.emailSubject }}"
},
"typeVersion": 2.1
},
{
"id": "bff930a7-dd30-46fe-affa-91fb42a36367",
"name": "13. Code \u2014 Extract Action Items",
"type": "n8n-nodes-base.code",
"position": [
-2880,
-64
],
"parameters": {
"jsCode": "// Extract action items from AI summary\nconst summaryRaw = $('11. Code \u2014 Format Email + Split Participants').item.json.summaryRaw;\nconst meetingTopic = $('11. Code \u2014 Format Email + Split Participants').item.json.meetingTopic;\n\nconst actionItemsMatch = summaryRaw.match(/## Action Items\\n([\\s\\S]*?)(?=\\n## |$)/);\nconst actionItemsSection = actionItemsMatch ? actionItemsMatch[1].trim() : '';\n\nif (!actionItemsSection || actionItemsSection.toLowerCase() === 'none') {\n return [{ json: { hasTasks: false, tasks: [] } }];\n}\n\nconst taskLines = actionItemsSection.split('\\n').filter(l => l.trim().startsWith('-'));\n\nconst tasks = taskLines.map(line => {\n const cleanLine = line.replace(/^-\\s*/, '').trim();\n const taskMatch = cleanLine.match(/^(.+?):\\s*(.+?)\\s*\\|\\s*Due:\\s*(.+?)\\s*\\|\\s*Priority:\\s*(High|Medium|Low)/i);\n\n if (taskMatch) {\n return {\n name: taskMatch[1].trim(),\n description: `Action item from meeting: ${meetingTopic}\\n\\nAssigned to: ${taskMatch[2].trim()}\\n\\nFull context: ${cleanLine}`,\n owner: taskMatch[2].trim(),\n dueDate: taskMatch[3].trim().toLowerCase() === 'not specified' ? '' : taskMatch[3].trim(),\n priority: taskMatch[4].toLowerCase()\n };\n }\n\n return {\n name: cleanLine.substring(0, 100),\n description: `Action item from meeting: ${meetingTopic}\\n\\nFull context: ${cleanLine}`,\n owner: 'Unassigned',\n dueDate: '',\n priority: 'normal'\n };\n}).filter(t => t.name.length > 2);\n\nreturn [{ json: { hasTasks: tasks.length > 0, tasks, taskCount: tasks.length } }];"
},
"typeVersion": 2
},
{
"id": "69f3c149-a08f-4a57-90d5-45b2a7684045",
"name": "14. IF \u2014 Has Action Items?",
"type": "n8n-nodes-base.if",
"position": [
-2800,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "has-tasks",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.hasTasks }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "43d2bb35-1793-439e-8203-205000d0fdfd",
"name": "15. Code \u2014 Split Tasks",
"type": "n8n-nodes-base.code",
"position": [
-2624,
-176
],
"parameters": {
"jsCode": "const tasks = $json.tasks || [];\nreturn tasks.map(task => ({ json: task }));"
},
"typeVersion": 2
},
{
"id": "90a79d0f-fd78-4fec-8219-262554233b92",
"name": "16. HTTP \u2014 Create ClickUp Task",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2400,
-176
],
"parameters": {
"url": "=https://api.clickup.com/api/v2/list/YOUR_CLICKUP_LIST_ID/task",
"method": "POST",
"options": {},
"jsonBody": "={\n \"name\": \"{{ $json.name }}\",\n \"description\": \"{{ $json.description }}\",\n \"priority\": {{ $json.priority === 'high' ? 1 : $json.priority === 'medium' ? 2 : 3 }},\n \"status\": \"to do\"\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "YOUR_CLICKUP_API_TOKEN"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "b921c7f8-db6d-43cd-bc73-022ed9b95a92",
"name": "17. Code \u2014 Extract Follow-Up Info",
"type": "n8n-nodes-base.code",
"position": [
-2608,
48
],
"parameters": {
"jsCode": "// Extract follow-up meeting info from AI summary\nconst summaryRaw = $('11. Code \u2014 Format Email + Split Participants').item.json.summaryRaw;\nconst meetingTopic = $('11. Code \u2014 Format Email + Split Participants').item.json.meetingTopic;\n\nconst followUpMatch = summaryRaw.match(/## Follow-up Meeting\\n([\\s\\S]*?)(?=\\n## |$)/);\nconst followUpSection = followUpMatch ? followUpMatch[1].trim() : '';\n\nif (!followUpSection || followUpSection.toLowerCase().includes('none scheduled')) {\n return [{ json: { hasFollowUp: false } }];\n}\n\nconst dateMatch = followUpSection.match(/Date:\\s*(.+)/);\nconst topicMatch = followUpSection.match(/Topic:\\s*(.+)/);\n\nconst followUpDate = dateMatch ? dateMatch[1].trim() : '';\nconst followUpTopic = topicMatch ? topicMatch[1].trim() : `Follow-up: ${meetingTopic}`;\n\nlet startDateTime;\nlet endDateTime;\ntry {\n const parsedDate = new Date(followUpDate);\n if (isNaN(parsedDate.getTime())) {\n const nextTuesday = new Date();\n nextTuesday.setDate(nextTuesday.getDate() + ((2 - nextTuesday.getDay() + 7) % 7 || 7));\n nextTuesday.setHours(10, 0, 0, 0);\n startDateTime = nextTuesday.toISOString();\n endDateTime = new Date(nextTuesday.getTime() + 60 * 60 * 1000).toISOString();\n } else {\n startDateTime = parsedDate.toISOString();\n endDateTime = new Date(parsedDate.getTime() + 60 * 60 * 1000).toISOString();\n }\n} catch (e) {\n const nextTuesday = new Date();\n nextTuesday.setDate(nextTuesday.getDate() + 7);\n nextTuesday.setHours(10, 0, 0, 0);\n startDateTime = nextTuesday.toISOString();\n endDateTime = new Date(nextTuesday.getTime() + 60 * 60 * 1000).toISOString();\n}\n\nreturn [{\n json: {\n hasFollowUp: true,\n followUpTitle: followUpTopic,\n startDateTime,\n endDateTime,\n originalSection: followUpSection\n }\n}];"
},
"typeVersion": 2
},
{
"id": "72d892f0-0580-4f76-aa28-0c9d98af6e73",
"name": "18. IF \u2014 Has Follow-Up?",
"type": "n8n-nodes-base.if",
"position": [
-2400,
48
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "has-followup",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.hasFollowUp }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "e6186c18-73da-4d31-9a08-3dd1bf040b28",
"name": "19. Google Calendar \u2014 Create Follow-Up",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-2032,
-80
],
"parameters": {
"end": "={{ $json.endDateTime }}",
"start": "={{ $json.startDateTime }}",
"calendar": {
"__rl": true,
"mode": "id",
"value": "YOUR_CALENDAR_ID"
},
"additionalFields": {
"summary": "={{ $json.followUpTitle }}",
"attendees": "={{ $('9. Zoom \u2014 Get All Participants').item.json.participants.map(p => p.user_email).join(',') }}",
"description": "=Follow-up meeting created automatically from Zoom meeting summary.\n\n{{ $json.originalSection }}"
}
},
"typeVersion": 1.3
},
{
"id": "25aa8ed1-0023-4bce-a2d6-3f3eb59d2857",
"name": "20. Slack \u2014 Send Team Notification",
"type": "n8n-nodes-base.slack",
"position": [
-1632,
48
],
"parameters": {
"text": "=*Meeting Summary Ready*\n\n*Meeting:* {{ $('11. Code \u2014 Format Email + Split Participants').item.json.meetingTopic }}\n*Date:* {{ $('11. Code \u2014 Format Email + Split Participants').item.json.formattedDate }}\n*Duration:* {{ $('11. Code \u2014 Format Email + Split Participants').item.json.meetingDuration }} minutes\n*Participants:* {{ $('11. Code \u2014 Format Email + Split Participants').item.json.totalParticipants }} people\n\n\u2705 Summary emailed to all participants\n{{ $('14. IF \u2014 Has Action Items?').item.json.hasTasks ? '\u2705 Action items created in ClickUp' : '\u23ed\ufe0f No action items found' }}\n{{ $('18. IF \u2014 Has Follow-Up?').item.json.hasFollowUp ? '\u2705 Follow-up meeting added to Google Calendar' : '\u23ed\ufe0f No follow-up meeting scheduled' }}",
"otherOptions": {}
},
"typeVersion": 2.3
},
{
"id": "14d0bb65-9487-4ce2-9f1a-53d10bfe046d",
"name": "21. Google Sheets \u2014 Log Meeting",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2000,
288
],
"parameters": {
"columns": {
"value": {
"Date": "={{ $('11. Code \u2014 Format Email + Split Participants').item.json.formattedDate }}",
"Meeting ID": "={{ $('11. Code \u2014 Format Email + Split Participants').item.json.meetingId }}",
"Participants": "={{ $('11. Code \u2014 Format Email + Split Participants').item.json.totalParticipants }}",
"Processed At": "={{ $now.toFormat('dd MMMM yyyy HH:mm') }}",
"Summary Sent": "Yes",
"Meeting Title": "={{ $('11. Code \u2014 Format Email + Split Participants').item.json.meetingTopic }}",
"Tasks Created": "={{ $('14. IF \u2014 Has Action Items?').item.json.hasTasks ? $('14. IF \u2014 Has Action Items?').item.json.taskCount + ' tasks' : 'None' }}",
"Duration (min)": "={{ $('11. Code \u2014 Format Email + Split Participants').item.json.meetingDuration }}",
"Follow-up Created": "={{ $('18. IF \u2014 Has Follow-Up?').item.json.hasFollowUp ? 'Yes' : 'No' }}"
},
"schema": [],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Meeting Log"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "54e3ff4c-3e3b-4bfd-88b8-e7913b47c1cb",
"name": "Note \u2014 Known Requirements",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5808,
608
],
"parameters": {
"color": 3,
"width": 1272,
"content": "## \u26a0\ufe0f Three Known Requirements\n1. Zoom Pro or higher required \u2014 Cloud Recording and Auto-Transcription must be enabled in Zoom settings. Free accounts do not have cloud transcription.\n2. No recording stops gracefully \u2014 node 5b uses StopAndError if no VTT transcript file is found. Check Zoom recording settings if this triggers.\n3. ClickUp task creation \u2014 tasks are only created when action items have the exact format from the AI prompt (Task: Owner | Due: Date | Priority: Level). Edit node 13 parsing if your AI output format differs."
},
"typeVersion": 1
}
],
"connections": {
"Think Tool": {
"ai_tool": [
[
{
"node": "10. Claude 3.7 \u2014 Generate Meeting Summary",
"type": "ai_tool",
"index": 0
}
]
]
},
"2. Manual Trigger": {
"main": [
[
{
"node": "3. Zoom \u2014 Get All Meetings",
"type": "main",
"index": 0
}
]
]
},
"15. Code \u2014 Split Tasks": {
"main": [
[
{
"node": "16. HTTP \u2014 Create ClickUp Task",
"type": "main",
"index": 0
}
]
]
},
"18. IF \u2014 Has Follow-Up?": {
"main": [
[
{
"node": "19. Google Calendar \u2014 Create Follow-Up",
"type": "main",
"index": 0
}
],
[
{
"node": "20. Slack \u2014 Send Team Notification",
"type": "main",
"index": 0
}
]
]
},
"14. IF \u2014 Has Action Items?": {
"main": [
[
{
"node": "15. Code \u2014 Split Tasks",
"type": "main",
"index": 0
}
],
[
{
"node": "17. Code \u2014 Extract Follow-Up Info",
"type": "main",
"index": 0
}
]
]
},
"3. Zoom \u2014 Get All Meetings": {
"main": [
[
{
"node": "4. Filter \u2014 Last 24 Hours Only",
"type": "main",
"index": 0
}
]
]
},
"1. Schedule \u2014 Every Day 9AM": {
"main": [
[
{
"node": "3. Zoom \u2014 Get All Meetings",
"type": "main",
"index": 0
}
]
]
},
"5. Zoom \u2014 Get Recording Data": {
"main": [
[
{
"node": "6. Code \u2014 Extract Transcript URL",
"type": "main",
"index": 0
}
],
[
{
"node": "5b. Stop \u2014 No Recording Available",
"type": "main",
"index": 0
}
]
]
},
"16. HTTP \u2014 Create ClickUp Task": {
"main": [
[
{
"node": "17. Code \u2014 Extract Follow-Up Info",
"type": "main",
"index": 0
}
]
]
},
"4. Filter \u2014 Last 24 Hours Only": {
"main": [
[
{
"node": "5. Zoom \u2014 Get Recording Data",
"type": "main",
"index": 0
}
]
]
},
"9. Zoom \u2014 Get All Participants": {
"main": [
[
{
"node": "10. Claude 3.7 \u2014 Generate Meeting Summary",
"type": "main",
"index": 0
}
]
]
},
"13. Code \u2014 Extract Action Items": {
"main": [
[
{
"node": "14. IF \u2014 Has Action Items?",
"type": "main",
"index": 0
}
]
]
},
"8. Code \u2014 Parse Transcript Text": {
"main": [
[
{
"node": "9. Zoom \u2014 Get All Participants",
"type": "main",
"index": 0
}
]
]
},
"6. Code \u2014 Extract Transcript URL": {
"main": [
[
{
"node": "7. Zoom \u2014 Download Transcript File",
"type": "main",
"index": 0
}
]
]
},
"17. Code \u2014 Extract Follow-Up Info": {
"main": [
[
{
"node": "18. IF \u2014 Has Follow-Up?",
"type": "main",
"index": 0
}
]
]
},
"20. Slack \u2014 Send Team Notification": {
"main": [
[
{
"node": "21. Google Sheets \u2014 Log Meeting",
"type": "main",
"index": 0
}
]
]
},
"7. Zoom \u2014 Download Transcript File": {
"main": [
[
{
"node": "8. Code \u2014 Parse Transcript Text",
"type": "main",
"index": 0
}
]
]
},
"Anthropic \u2014 Claude 3.7 Sonnet Model": {
"ai_languageModel": [
[
{
"node": "10. Claude 3.7 \u2014 Generate Meeting Summary",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"12. Gmail \u2014 Send to Each Participant": {
"main": [
[
{
"node": "13. Code \u2014 Extract Action Items",
"type": "main",
"index": 0
}
]
]
},
"19. Google Calendar \u2014 Create Follow-Up": {
"main": [
[
{
"node": "20. Slack \u2014 Send Team Notification",
"type": "main",
"index": 0
}
]
]
},
"10. Claude 3.7 \u2014 Generate Meeting Summary": {
"main": [
[
{
"node": "11. Code \u2014 Format Email + Split Participants",
"type": "main",
"index": 0
}
]
]
},
"11. Code \u2014 Format Email + Split Participants": {
"main": [
[
{
"node": "12. Gmail \u2014 Send to Each Participant",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Activate this workflow once and every day at 9AM it automatically processes all Zoom meetings from the past 24 hours — no manual action needed after any call. For each recorded meeting, it downloads the transcript, fetches all participants, and uses Claude 3.7 Sonnet with the…
Source: https://n8n.io/workflows/15818/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This advanced n8n workflow is designed for web developers, system administrators, security analysts, and agency owners who need to automate the monitoring of website security posture. It acts as a vir
Author: Hyrum Hurst, AI Automation Engineer at QuarterSmart Contact: hyrum@quartersmart.com
Who is this for? Agencies, consultants, and service providers who conduct discovery calls and need to quickly turn conversations into professional proposals.
Created by: Peyton Leveillee Last updated: October 2025
The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe