🎯 Meeting Automation
🎯 Mục tiêu bài học
Sau bài học này, bạn sẽ:
✅ Xây dựng smart scheduling system (Calendly-style)
✅ Tạo meeting prep automation với attendee research
✅ Implement meeting notes, transcription, và action items extraction
✅ Thiết kế post-meeting follow-up và meeting analytics
Meetings consume significant time. Bài này hướng dẫn automate toàn bộ meeting lifecycle từ scheduling đến follow-up.
🔍 Meeting Automation Overview
Full Meeting Lifecycle:
Checkpoint
Meeting automation lifecycle bao gồm mấy giai đoạn?
⚡ Workflow 1: Smart Scheduling
Calendly-Style Self-Scheduling:
Meeting Scheduler
Find Available Slots:
1const request = $input.item.json;2const busySlots = $('Get Calendar').all();34// Configuration5const config = {6 workingHours: { start: 9, end: 18 },7 meetingDuration: request.duration || 30, // minutes8 bufferBefore: 15, // minutes9 bufferAfter: 15,10 daysAhead: 14,11 timezone: 'Asia/Ho_Chi_Minh'12};1314const now = new Date();15const slots = [];1617// Check next 14 days18for (let day = 1; day <= config.daysAhead; day++) {19 const checkDate = new Date(now);20 checkDate.setDate(now.getDate() + day);21 22 // Skip weekends23 if (checkDate.getDay() === 0 || checkDate.getDay() === 6) continue;24 25 // Check each 30-min slot26 for (let hour = config.workingHours.start; hour < config.workingHours.end; hour++) {27 for (let min = 0; min < 60; min += 30) {28 const slotStart = new Date(checkDate);29 slotStart.setHours(hour, min, 0, 0);30 31 const slotEnd = new Date(slotStart);32 slotEnd.setMinutes(slotEnd.getMinutes() + config.meetingDuration);33 34 // Apply buffer35 const bufferStart = new Date(slotStart);36 bufferStart.setMinutes(bufferStart.getMinutes() - config.bufferBefore);37 38 const bufferEnd = new Date(slotEnd);39 bufferEnd.setMinutes(bufferEnd.getMinutes() + config.bufferAfter);40 41 // Check for conflicts42 const hasConflict = busySlots.some(busy => {43 const busyStart = new Date(busy.json.start.dateTime);44 const busyEnd = new Date(busy.json.end.dateTime);45 return (bufferStart < busyEnd && bufferEnd > busyStart);46 });47 48 if (!hasConflict) {49 slots.push({50 start: slotStart.toISOString(),51 end: slotEnd.toISOString(),52 display: slotStart.toLocaleString('en-US', {53 weekday: 'short',54 month: 'short',55 day: 'numeric',56 hour: '2-digit',57 minute: '2-digit'58 })59 });60 }61 }62 }63}6465return { availableSlots: slots.slice(0, 20) }; // Return top 20Create Meeting Event:
1{2 "name": "Create Calendar Event",3 "type": "n8n-nodes-base.googleCalendar",4 "parameters": {5 "operation": "create",6 "calendar": "primary",7 "start": "={{ $json.selectedSlot.start }}",8 "end": "={{ $json.selectedSlot.end }}",9 "summary": "={{ $json.meetingType }}: {{ $json.guestName }}",10 "description": "Scheduled via automated booking\n\nGuest: {{ $json.guestEmail }}\nTopic: {{ $json.topic }}",11 "attendees": "={{ $json.guestEmail }}",12 "conferenceData": {13 "createRequest": {14 "conferenceSolutionKey": {15 "type": "hangoutsMeet"16 }17 }18 },19 "sendUpdates": "all"20 }21}Checkpoint
Smart scheduling kiểm tra available slots trong bao nhiêu ngày?
🔧 Workflow 2: Meeting Prep Automation
Auto-Generate Prep Materials:
Meeting Prep
Attendee Research:
1const event = $input.item.json;2const attendees = event.attendees.filter(a => !a.self);34const prepData = {5 meetingTitle: event.summary,6 meetingTime: event.start.dateTime,7 attendees: []8};910for (const attendee of attendees) {11 // Get CRM data (from previous node)12 const crmData = $('Get CRM').all()13 .find(c => c.json.email === attendee.email);14 15 // Get previous meetings (from previous node)16 const previousMeetings = $('Get Previous Meetings').all()17 .filter(m => m.json.attendees?.some(a => a.email === attendee.email))18 .slice(0, 3);19 20 prepData.attendees.push({21 email: attendee.email,22 name: attendee.displayName || attendee.email.split('@')[0],23 company: crmData?.json.company || 'Unknown',24 role: crmData?.json.role || 'Unknown',25 lastContact: crmData?.json.lastContact,26 dealStage: crmData?.json.dealStage,27 previousMeetings: previousMeetings.map(m => ({28 date: m.json.start.dateTime,29 topic: m.json.summary,30 notes: m.json.description?.substring(0, 100)31 }))32 });33}3435return prepData;Generate Prep Document:
1const prep = $input.item.json;23const document = `4# Meeting Prep: ${prep.meetingTitle}56�� **When:** ${new Date(prep.meetingTime).toLocaleString()}78## �� Attendees910${prep.attendees.map(a => `11### ${a.name}12- **Company:** ${a.company}13- **Role:** ${a.role}14- **Deal Stage:** ${a.dealStage || 'N/A'}15- **Last Contact:** ${a.lastContact || 'Unknown'}1617**Previous Meetings:**18${a.previousMeetings.length > 0 19 ? a.previousMeetings.map(m => `- ${m.date}: ${m.topic}`).join('\n')20 : '- No previous meetings recorded'}21`).join('\n---\n')}2223## �� Agenda241. 252. 263. 2728## ❓ Questions to Ask29- 30- 3132## �� Meeting Objectives33- [ ] 34- [ ] 3536## �� Notes37*(To be filled during meeting)*3839## ✅ Action Items40*(To be filled after meeting)*4142*Generated automatically by n8n*43`;4445return { content: document, title: `Prep: ${prep.meetingTitle}` };Checkpoint
Meeting prep automation research attendees từ những nguồn nào?
🏗️ Workflow 3: Meeting Notes & Transcription
Auto-Create Meeting Notes:
Meeting Notes
Meeting Notes Template:
1const meeting = $input.item.json;23const notesTemplate = `4# �� ${meeting.summary}56**Date:** ${new Date(meeting.start.dateTime).toLocaleString()}7**Attendees:** ${meeting.attendees.map(a => a.email).join(', ')}8**Location:** ${meeting.hangoutLink || meeting.location || 'N/A'}910## �� Agenda11${meeting.description || '*No agenda provided*'}1213## �� Discussion Notes1415### Topic 1:161718### Topic 2:192021### Topic 3:2223## ✅ Action Items24*Format: - [ ] Task @assignee due:date*2526- [ ] 27- [ ] 28- [ ] 2930## �� Decisions Made31- 3233## �� Next Steps34- 3536## ❓ Open Questions37- 3839*Notes auto-generated. Please update during/after meeting.*40`;4142return { template: notesTemplate };Extract Action Items:
1const notes = $input.item.json.content;23// Parse action items4const actionItemRegex = /- \[ \]\s*(.+?)(?:\s*@(\w+))?(?:\s*due:?(\d{1,2}\/\d{1,2}))?(?:\n|$)/gi;5const matches = [...notes.matchAll(actionItemRegex)];67const actionItems = matches.map((match, index) => ({8 id: index + 1,9 task: match[1].trim().replace(/@\w+/, '').replace(/due:\d{1,2}\/\d{1,2}/i, '').trim(),10 assignee: match[2] || 'unassigned',11 dueDate: match[3] || null,12 source: 'meeting_notes',13 meetingId: $input.item.json.meetingId14}));1516// Parse decisions17const decisionsRegex = /## �� Decisions Made\n([\s\S]*?)(?=\n---|\n##|$)/i;18const decisionsMatch = notes.match(decisionsRegex);19const decisions = decisionsMatch 20 ? decisionsMatch[1].split('\n').filter(d => d.trim().startsWith('-')).map(d => d.replace(/^-\s*/, ''))21 : [];2223return {24 actionItems,25 decisions,26 actionCount: actionItems.length,27 decisionCount: decisions.length28};Checkpoint
Meeting notes template bao gồm những sections nào?
📊 Workflow 4: Post-Meeting Follow-Up
Automated Follow-Up Email:
Meeting Follow-Up
Generate Follow-Up Email:
1const meeting = $input.item.json;2const notes = $('Parse Notes').item.json;34const attendeeNames = meeting.attendees5 .filter(a => !a.self)6 .map(a => a.displayName || a.email.split('@')[0]);78const emailBody = `9Hi ${attendeeNames.length === 1 ? attendeeNames[0] : 'everyone'},1011Thank you for attending today's meeting: **${meeting.summary}**.1213## Summary14${notes.summary || 'Meeting summary to be added.'}1516## Key Decisions17${notes.decisions.length > 0 18 ? notes.decisions.map(d => `• ${d}`).join('\n')19 : '• No formal decisions recorded'}2021## Action Items22${notes.actionItems.length > 023 ? notes.actionItems.map(a => `• ${a.task}${a.assignee !== 'unassigned' ? ` (@${a.assignee})` : ''}${a.dueDate ? ` - Due: ${a.dueDate}` : ''}`).join('\n')24 : '• No action items recorded'}2526## Next Steps27${notes.nextSteps || 'To be determined'}2829�� Full meeting notes: ${notes.notesUrl}30�� Recording: ${meeting.recordingUrl || 'Not available'}3132Please let me know if I missed anything or if you have questions.3334Best regards,35${meeting.organizer}3637*This follow-up was automatically generated.*38`;3940return {41 to: meeting.attendees.filter(a => !a.self).map(a => a.email),42 subject: `Follow-up: ${meeting.summary}`,43 body: emailBody,44 replyTo: meeting.organizer.email45};Checkpoint
Post-meeting follow-up email bao gồm những phần nào?
💡 Workflow 5: Recurring Meeting Analytics
Meeting Metrics Dashboard:
Meeting Analytics
Meeting Analytics Code:
1const meetings = $input.all();23const analytics = {4 totalMeetings: 0,5 totalMinutes: 0,6 byType: {},7 byDayOfWeek: {Mon: 0, Tue: 0, Wed: 0, Thu: 0, Fri: 0},8 recurring: 0,9 oneOnOne: 0,10 largeGroup: 0,11 longestMeeting: null,12 shortestMeeting: null,13 externalMeetings: 014};1516for (const meeting of meetings) {17 const m = meeting.json;18 19 // Skip all-day events20 if (!m.start.dateTime) continue;21 22 analytics.totalMeetings++;23 24 // Duration25 const start = new Date(m.start.dateTime);26 const end = new Date(m.end.dateTime);27 const duration = (end - start) / (1000 * 60);28 analytics.totalMinutes += duration;29 30 // Track longest/shortest31 if (!analytics.longestMeeting || duration > analytics.longestMeeting.duration) {32 analytics.longestMeeting = { title: m.summary, duration };33 }34 if (!analytics.shortestMeeting || duration < analytics.shortestMeeting.duration) {35 analytics.shortestMeeting = { title: m.summary, duration };36 }37 38 // By day of week39 const day = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][start.getDay()];40 if (analytics.byDayOfWeek[day] !== undefined) {41 analytics.byDayOfWeek[day]++;42 }43 44 // Meeting type45 const attendeeCount = m.attendees?.length || 1;46 if (attendeeCount === 2) {47 analytics.oneOnOne++;48 } else if (attendeeCount > 5) {49 analytics.largeGroup++;50 }51 52 // Recurring check53 if (m.recurringEventId) {54 analytics.recurring++;55 }56 57 // External check58 const hasExternal = m.attendees?.some(a => 59 !a.email.includes('@yourcompany.com')60 );61 if (hasExternal) {62 analytics.externalMeetings++;63 }64 65 // By type (from title)66 let type = 'Other';67 const title = m.summary.toLowerCase();68 if (title.includes('standup')) type = 'Standup';69 else if (title.includes('1:1') || title.includes('1-1')) type = '1:1';70 else if (title.includes('review')) type = 'Review';71 else if (title.includes('planning')) type = 'Planning';72 73 analytics.byType[type] = (analytics.byType[type] || 0) + 1;74}7576// Calculate derived metrics77analytics.avgDuration = Math.round(analytics.totalMinutes / analytics.totalMeetings);78analytics.hoursInMeetings = (analytics.totalMinutes / 60).toFixed(1);79analytics.percentOfWeek = ((analytics.totalMinutes / (40 * 60)) * 100).toFixed(1);80analytics.busiestDay = Object.entries(analytics.byDayOfWeek)81 .sort((a, b) => b[1] - a[1])[0][0];8283return analytics;Checkpoint
Meeting analytics tính toán percentOfWeek bằng cách nào?
🌟 Workflow 6: Meeting Recording Management
Organize Meeting Recordings:
Recording Handler
Recording Organization:
1const recording = $input.item.json;2const meeting = $('Get Meeting Details').item.json;34// Generate standardized name5const date = new Date(meeting.start.dateTime);6const dateStr = date.toISOString().split('T')[0];7const meetingType = detectMeetingType(meeting.summary);89const newName = `${dateStr}_${meetingType}_${sanitize(meeting.summary)}`;1011// Determine folder based on type12const folderMap = {13 'Standup': 'Recordings/Standups',14 '1:1': 'Recordings/1-on-1s',15 'Client': 'Recordings/Client Calls',16 'Team': 'Recordings/Team Meetings',17 'Other': 'Recordings/General'18};1920const destinationFolder = folderMap[meetingType] || folderMap['Other'];2122// Set retention policy23const retentionDays = meetingType === 'Client' ? 365 : 90;24const deleteDate = new Date();25deleteDate.setDate(deleteDate.getDate() + retentionDays);2627return {28 originalName: recording.name,29 newName,30 destinationFolder,31 deleteDate: deleteDate.toISOString(),32 attendeesToNotify: meeting.attendees.map(a => a.email),33 metadata: {34 meetingTitle: meeting.summary,35 date: dateStr,36 duration: recording.duration,37 type: meetingType38 }39};4041function detectMeetingType(title) {42 const t = title.toLowerCase();43 if (t.includes('standup')) return 'Standup';44 if (t.includes('1:1') || t.includes('1-1')) return '1:1';45 if (t.includes('client') || t.includes('customer')) return 'Client';46 if (t.includes('team') || t.includes('sync')) return 'Team';47 return 'Other';48}4950function sanitize(str) {51 return str.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 50);52}Checkpoint
Recording organization đặt retention policy bao lâu cho client calls?
📋 Best Practices
DO:
- ✅ Send prep materials in advance
- ✅ Follow up within 24 hours
- ✅ Extract clear action items
- ✅ Track meeting metrics
- ✅ Respect calendar boundaries
DON'T:
- ❌ Over-automate personal interactions
- ❌ Send generic follow-ups
- ❌ Ignore declined meetings
- ❌ Record without consent
Checkpoint
Tại sao không nên record meeting without consent?
📝 Bài Tập Thực Hành
Build complete meeting system:
- Create self-scheduling workflow
- Build meeting prep automation
- Set up notes template generator
- Implement post-meeting follow-up
- Design meeting analytics dashboard
Own your meetings! 🎯
Checkpoint
Bạn đã hoàn thành những challenges nào trong bài tập?
🧠 Key Takeaways
- 📅 Prep = Success - Automate preparation
- 📝 Capture everything - Notes + recordings
- ✅ Follow up fast - Within 24 hours
- 📊 Track metrics - Know where time goes
- 🤝 Keep it human - Automation supports, doesn't replace
Checkpoint
Tại sao nên follow up within 24 hours?
🚀 Bài tiếp theo
Onboarding Workflows — Automate new team member setup và training.
