This workflow corresponds to n8n.io template #14959 — we link there as the canonical source.
This workflow follows the Agent → Google Sheets 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": "e790d56c-8b02-483b-8015-455c734ab36d",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1952,
32
],
"parameters": {
"width": 556,
"height": 1124,
"content": "## Zoom Attendance Tracker \u2014 Google Sheets + ClickUp + Telegram\n\nFor team leads, trainers, and operations managers who run recurring Zoom meetings and want automatic attendance logging with late-participant follow-up. When a Zoom meeting ends, this workflow fires automatically. Every participant is classified as Attended or Late, and each row is logged to Google Sheets. The host gets one AI-written Telegram summary per meeting. For every participant who joined late, a ClickUp follow-up task is created automatically so nothing slips through.\n\n## How it works\n- **1. Webhook \u2014 Zoom Meeting Ended** receives the meeting.ended event from Zoom\n- **2. Set \u2014 Config Values** stores Telegram chat ID, Sheet ID, host name, late threshold, and ClickUp list ID\n- **3. Code \u2014 Extract and Classify Participants** parses all participants and classifies each as Attended or Late based on join delay\n- **4. Google Sheets \u2014 Log Participant Row** logs every participant as one row \u2014 runs for all items\n- **5. IF \u2014 First Row Check** gates the Telegram path \u2014 only the first participant item passes\n- **6. AI Agent \u2014 Write Telegram Summary** writes a concise meeting summary for the host\n- **12. Telegram \u2014 Send Meeting Summary** delivers the summary once per meeting\n- **8. IF \u2014 Is Participant Late?** checks each non-first participant for late status\n- **9. ClickUp \u2014 Create Late Participant Task** creates a follow-up task for each late joiner\n- **10. Set \u2014 No Action Needed** provides a clean exit for on-time participants\n\n## Set up steps\n1. In **2. Set \u2014 Config Values** \u2014 replace all six values: Telegram chat ID, Google Sheet ID, sheet tab name, host name, late threshold minutes, and ClickUp list ID\n2. In **1. Webhook \u2014 Zoom Meeting Ended** \u2014 copy the webhook URL and add it in Zoom Marketplace under your Webhook Only App for the meeting.ended event\n3. In **4. Google Sheets \u2014 Log Participant Row** \u2014 connect your Google Sheets OAuth2 credential\n4. In **7. OpenAI \u2014 GPT-4o-mini Model** \u2014 connect your OpenAI credential\n5. In **9. ClickUp \u2014 Create Late Participant Task** \u2014 connect your ClickUp API credential\n6. In **12. Telegram \u2014 Send Meeting Summary** \u2014 connect your Telegram Bot API credential\n7. Activate the workflow before running your next Zoom meeting"
},
"typeVersion": 1
},
{
"id": "d1b80950-ebdf-45b2-b6e5-c420cc90b22d",
"name": "Section \u2014 Webhook and Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1344,
288
],
"parameters": {
"color": 5,
"width": 436,
"height": 356,
"content": "## Webhook and Config\nZoom sends a POST request here when any meeting ends. Config stores Telegram chat ID, Sheet ID, host name, late threshold in minutes, and ClickUp list ID \u2014 all set once here."
},
"typeVersion": 1
},
{
"id": "e968d982-3f6c-435f-aa94-01b8b4b8b3b9",
"name": "Section \u2014 Participant Extraction",
"type": "n8n-nodes-base.stickyNote",
"position": [
-880,
288
],
"parameters": {
"color": 6,
"width": 352,
"height": 356,
"content": "## Participant Extraction\nParses all participants from the Zoom payload. Classifies each as Attended or Late based on join delay vs the threshold. Outputs one item per participant \u2014 every downstream node runs once per person."
},
"typeVersion": 1
},
{
"id": "a3eaf482-120a-491a-96a5-46493f47fcb1",
"name": "Section \u2014 Sheet Logging",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
-48
],
"parameters": {
"color": 4,
"height": 388,
"content": "## Sheet Logging\nLogs every participant as one row \u2014 runs for all items regardless of status. No condition. 14-column record per person."
},
"typeVersion": 1
},
{
"id": "27edc4bc-4669-4efb-bd57-5aa49fa51e77",
"name": "Section \u2014 First Row Gate",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
384
],
"parameters": {
"color": 6,
"width": 288,
"height": 420,
"content": "## First Row Gate\nPasses only the first participant item (isFirstRow = true) to the Telegram path. Prevents the host from receiving one message per participant."
},
"typeVersion": 1
},
{
"id": "bb6944ec-b0dd-4d52-a3d2-f8436a404fe9",
"name": "Section \u2014 Telegram Summary",
"type": "n8n-nodes-base.stickyNote",
"position": [
16,
208
],
"parameters": {
"color": 6,
"width": 788,
"height": 516,
"content": "## Telegram Summary\nGPT-4o-mini writes a concise host-facing meeting summary covering topic, attendance counts, one observation, and one action tip. Fires exactly once per meeting via Telegram."
},
"typeVersion": 1
},
{
"id": "fb127160-98c6-4450-ace1-fad021360e86",
"name": "Section \u2014 Late Participant Actions",
"type": "n8n-nodes-base.stickyNote",
"position": [
32,
800
],
"parameters": {
"color": 4,
"width": 532,
"height": 564,
"content": "## Late Participant Actions\nFor each non-first participant, checks if status is Late. Late joiners trigger a ClickUp follow-up task with full meeting context and a next-day due date. On-time participants exit cleanly."
},
"typeVersion": 1
},
{
"id": "a6732b48-b745-4998-a138-f8028165be4d",
"name": "1. Webhook \u2014 Zoom Meeting Ended",
"type": "n8n-nodes-base.webhook",
"position": [
-1296,
464
],
"parameters": {
"path": "zoom-meeting-ended",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 1.1
},
{
"id": "038ad51e-f770-4506-85e8-d424743a6f4c",
"name": "2. Set \u2014 Config Values",
"type": "n8n-nodes-base.set",
"position": [
-1072,
464
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-001",
"name": "telegramChatId",
"type": "string",
"value": "YOUR_TELEGRAM_CHAT_ID"
},
{
"id": "cfg-002",
"name": "sheetId",
"type": "string",
"value": "YOUR_GOOGLE_SHEET_ID"
},
{
"id": "cfg-003",
"name": "sheetName",
"type": "string",
"value": "Attendance Log"
},
{
"id": "cfg-004",
"name": "hostName",
"type": "string",
"value": "YOUR HOST NAME"
},
{
"id": "cfg-005",
"name": "lateThresholdMinutes",
"type": "string",
"value": "5"
},
{
"id": "cfg-006",
"name": "clickupListId",
"type": "string",
"value": "YOUR_CLICKUP_LIST_ID"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "7237e5c2-e629-42e8-b42e-2e3b65679d2e",
"name": "3. Code \u2014 Extract and Classify Participants",
"type": "n8n-nodes-base.code",
"position": [
-752,
464
],
"parameters": {
"jsCode": "const body = $input.first().json;\nconst config = $('2. Set \u2014 Config Values').item.json;\n\nconst payload = body.payload || body;\nconst obj = payload.object || {};\n\nconst meetingId = obj.id || obj.meeting_id || 'unknown';\nconst topic = obj.topic || 'Untitled Meeting';\nconst hostEmail = obj.host_email || payload.host_email || 'unknown';\nconst hostName = config.hostName || hostEmail;\nconst startTime = obj.start_time || new Date().toISOString();\nconst durationMinutes = obj.duration || 0;\nconst participants = obj.participants || [];\n\nconst meetingStart = new Date(startTime);\nconst lateThreshold = parseInt(config.lateThresholdMinutes) || 5;\n\nconst attendanceRows = [];\nlet attendedCount = 0;\nlet lateCount = 0;\n\nparticipants.forEach(p => {\n const name = p.user_name || p.name || 'Unknown';\n const email = p.user_email || p.email || 'No email';\n const joinTime = p.join_time ? new Date(p.join_time) : null;\n const leaveTime = p.leave_time ? new Date(p.leave_time) : null;\n const stayDuration = p.duration || 0;\n\n let joinDelayMin = 0;\n let status = 'Attended';\n\n if (joinTime) {\n joinDelayMin = Math.round((joinTime - meetingStart) / 60000);\n if (joinDelayMin > lateThreshold) {\n status = 'Late';\n lateCount++;\n } else {\n attendedCount++;\n }\n } else {\n status = 'Attended';\n attendedCount++;\n }\n\n const stayMinutes = Math.round(stayDuration / 60);\n const joinTimeStr = joinTime ? joinTime.toISOString().replace('T',' ').substring(0,16) : 'N/A';\n const leaveTimeStr = leaveTime ? leaveTime.toISOString().replace('T',' ').substring(0,16) : 'N/A';\n\n attendanceRows.push({\n meetingId,\n meetingTopic: topic,\n hostEmail,\n meetingDate: meetingStart.toISOString().split('T')[0],\n meetingStartTime: meetingStart.toISOString().replace('T',' ').substring(0,16),\n meetingDurationMin: durationMinutes,\n participantName: name,\n participantEmail: email,\n joinTime: joinTimeStr,\n leaveTime: leaveTimeStr,\n stayDuration: `${stayMinutes} min`,\n joinDelayMin: joinDelayMin,\n status: status,\n loggedAt: new Date().toISOString().replace('T',' ').substring(0,16)\n });\n});\n\nconst totalParticipants = participants.length;\n\nreturn attendanceRows.map((row, index) => ({\n json: {\n ...row,\n isFirstRow: index === 0,\n totalParticipants,\n attendedCount,\n lateCount,\n topic,\n meetingId,\n hostName,\n hostEmail,\n telegramChatId: config.telegramChatId,\n sheetId: config.sheetId,\n sheetName: config.sheetName,\n clickupListId: config.clickupListId,\n meetingDurationMin: durationMinutes,\n meetingDate: meetingStart.toISOString().split('T')[0],\n lateThresholdMinutes: config.lateThresholdMinutes\n }\n}));"
},
"typeVersion": 2
},
{
"id": "910395bd-3476-43bc-9356-7b9b9994c85b",
"name": "4. Google Sheets \u2014 Log Participant Row",
"type": "n8n-nodes-base.googleSheets",
"position": [
-320,
128
],
"parameters": {
"columns": {
"value": {
"Status": "={{ $json.status }}",
"Join Time": "={{ $json.joinTime }}",
"Logged At": "={{ $json.loggedAt }}",
"Host Email": "={{ $json.hostEmail }}",
"Leave Time": "={{ $json.leaveTime }}",
"Meeting ID": "={{ $json.meetingId }}",
"Meeting Date": "={{ $json.meetingDate }}",
"Meeting Topic": "={{ $json.meetingTopic }}",
"Time in Meeting": "={{ $json.stayDuration }}",
"Join Delay (min)": "={{ $json.joinDelayMin }}",
"Participant Name": "={{ $json.participantName }}",
"Participant Email": "={{ $json.participantEmail }}",
"Meeting Start Time": "={{ $json.meetingStartTime }}",
"Meeting Duration (min)": "={{ $json.meetingDurationMin }}"
},
"mappingMode": "defineBelow"
},
"options": {
"cellFormat": "USER_ENTERED"
},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $json.sheetName }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.sheetId }}"
}
},
"typeVersion": 4.5
},
{
"id": "1122ac71-d786-4c9c-9ae6-4fe818704c7b",
"name": "5. IF \u2014 First Row Check",
"type": "n8n-nodes-base.if",
"position": [
-400,
576
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-001",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.isFirstRow }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "eb000ad3-cd34-45a8-8ffe-45f4cba5fd77",
"name": "6. AI Agent \u2014 Write Telegram Summary",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
96,
368
],
"parameters": {
"text": "=You are an assistant that writes short, professional meeting attendance summaries for a team host.\n\nMEETING DETAILS:\nTopic: {{ $json.topic }}\nMeeting ID: {{ $json.meetingId }}\nHost: {{ $json.hostName }}\nDate: {{ $json.meetingDate }}\nDuration: {{ $json.meetingDurationMin }} minutes\nTotal Participants Who Joined: {{ $json.totalParticipants }}\nJoined On Time: {{ $json.attendedCount }}\nJoined Late (after {{ $json.lateThresholdMinutes }} min): {{ $json.lateCount }}\n\nWrite a short Telegram notification message for the host. Follow this format exactly:\n\nLine 1: Meeting just ended \u2014 state the topic name\nLine 2: Blank line\nLine 3: Date and duration\nLine 4: Total participants count\nLine 5: On-time count\nLine 6: Late arrivals count\nLine 7: Blank line\nLine 8: One sentence observation \u2014 was attendance good or poor, anything notable\nLine 9: One short action suggestion for the host based on attendance numbers\nLine 10: Blank line\nLine 11: End with: Full attendance log saved to Google Sheets.\n\nRULES:\n- Plain text only. No markdown. No asterisks.\n- Maximum 80 words total.\n- Be direct and professional.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "42d9ef24-c11b-4c28-8ae6-ad8cc09553ab",
"name": "7. OpenAI \u2014 GPT-4o-mini Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
96,
560
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini"
},
"options": {
"maxTokens": 200,
"temperature": 0.4
}
},
"typeVersion": 1.2
},
{
"id": "3b620667-b1b3-4362-be43-aa7461fa2745",
"name": "8. IF \u2014 Is Participant Late?",
"type": "n8n-nodes-base.if",
"position": [
80,
1040
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "cond-late-001",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "Late"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "6e2fad45-d8e1-4370-b557-f4a7ff64fc8f",
"name": "9. ClickUp \u2014 Create Late Participant Task",
"type": "n8n-nodes-base.clickUp",
"position": [
352,
928
],
"parameters": {
"list": "={{ $json.clickupListId }}",
"name": "=Follow-Up: {{ $json.participantName }} joined late \u2014 {{ $json.meetingTopic }}",
"authentication": "oAuth2",
"additionalFields": {
"content": "=LATE PARTICIPANT FOLLOW-UP\n\nMeeting: {{ $json.meetingTopic }}\nMeeting ID: {{ $json.meetingId }}\nDate: {{ $json.meetingDate }}\n\nParticipant: {{ $json.participantName }}\nEmail: {{ $json.participantEmail }}\nJoined: {{ $json.joinTime }} ({{ $json.joinDelayMin }} minutes late)\nLeft: {{ $json.leaveTime }}\nTime in Meeting: {{ $json.stayDuration }}\n\nACTION REQUIRED:\n- Check if participant missed any key decisions\n- Send them the meeting recording or notes if needed\n- Confirm they received all action items assigned to them\n\nAuto-created by n8n Zoom Attendance Tracker",
"dueDate": "={{ $now.plus({days: 1}).toMillis() }}",
"priority": 3
}
},
"typeVersion": 1
},
{
"id": "fa7a1da4-7cb9-43fa-9e9c-f56cb465443d",
"name": "10. Set \u2014 No Action Needed",
"type": "n8n-nodes-base.set",
"position": [
352,
1152
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "noaction-001",
"name": "result",
"type": "string",
"value": "=Participant {{ $json.participantName }} attended on time. No follow-up needed."
},
{
"id": "noaction-002",
"name": "participantName",
"type": "string",
"value": "={{ $json.participantName }}"
},
{
"id": "noaction-003",
"name": "status",
"type": "string",
"value": "={{ $json.status }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "211cb559-1dc0-4389-ba37-1902b0d10907",
"name": "11. Set \u2014 Prepare Telegram Fields",
"type": "n8n-nodes-base.set",
"position": [
352,
368
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "tg-001",
"name": "telegramMessage",
"type": "string",
"value": "={{ $json.output || 'Attendance logged. Check Google Sheets for full report.' }}"
},
{
"id": "tg-002",
"name": "telegramChatId",
"type": "string",
"value": "={{ $('3. Code \u2014 Extract and Classify Participants').first().json.telegramChatId }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "743f9300-7e64-44f9-8149-c50f84e61522",
"name": "12. Telegram \u2014 Send Meeting Summary",
"type": "n8n-nodes-base.telegram",
"position": [
592,
368
],
"parameters": {
"text": "={{ $json.telegramMessage }}",
"chatId": "={{ $json.telegramChatId }}",
"additionalFields": {
"appendAttribution": false
}
},
"typeVersion": 1.2
}
],
"connections": {
"2. Set \u2014 Config Values": {
"main": [
[
{
"node": "3. Code \u2014 Extract and Classify Participants",
"type": "main",
"index": 0
}
]
]
},
"5. IF \u2014 First Row Check": {
"main": [
[
{
"node": "6. AI Agent \u2014 Write Telegram Summary",
"type": "main",
"index": 0
}
],
[
{
"node": "8. IF \u2014 Is Participant Late?",
"type": "main",
"index": 0
}
]
]
},
"8. IF \u2014 Is Participant Late?": {
"main": [
[
{
"node": "9. ClickUp \u2014 Create Late Participant Task",
"type": "main",
"index": 0
}
],
[
{
"node": "10. Set \u2014 No Action Needed",
"type": "main",
"index": 0
}
]
]
},
"7. OpenAI \u2014 GPT-4o-mini Model": {
"ai_languageModel": [
[
{
"node": "6. AI Agent \u2014 Write Telegram Summary",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"1. Webhook \u2014 Zoom Meeting Ended": {
"main": [
[
{
"node": "2. Set \u2014 Config Values",
"type": "main",
"index": 0
}
]
]
},
"11. Set \u2014 Prepare Telegram Fields": {
"main": [
[
{
"node": "12. Telegram \u2014 Send Meeting Summary",
"type": "main",
"index": 0
}
]
]
},
"6. AI Agent \u2014 Write Telegram Summary": {
"main": [
[
{
"node": "11. Set \u2014 Prepare Telegram Fields",
"type": "main",
"index": 0
}
]
]
},
"3. Code \u2014 Extract and Classify Participants": {
"main": [
[
{
"node": "4. Google Sheets \u2014 Log Participant Row",
"type": "main",
"index": 0
},
{
"node": "5. IF \u2014 First Row Check",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
When a Zoom meeting ends, this workflow fires automatically — no manual action needed. It classifies every participant as On Time or Late, logs all attendance data to Google Sheets, and sends you one AI-written summary via Telegram. For every late joiner, a ClickUp follow-up…
Source: https://n8n.io/workflows/14959/ — 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.
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.
🧠 Gwen – The AI Voice Marketing Agent Gwen is your intelligent voice-powered marketing assistant built in n8n. She combines the power of OpenAI, ElevenLabs, and automation workflows to handle content
Universal Expense tracker. Uses telegram, httpRequest, openAi, googleSheets. Webhook trigger; 33 nodes.
This workflow captures website form submissions, automatically scores leads using AI based on custom criteria, stores data in Google Sheets, sends instant notifications to your sales team via Telegram
##📘 Description This workflow acts as a real-time emergency alert system designed for personal safety scenarios. It receives distress signals via webhook, enriches the data with a live Google Maps lin