This workflow corresponds to n8n.io template #16230 — 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 →
{
"id": "49Hzuh4ryzl2siI1",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Analyze Google Calendar patterns and send weekly optimization digest via Gmail",
"tags": [
{
"id": "5uYTxQdc9i0HSzQG",
"name": "productivity",
"createdAt": "2026-06-10T07:28:09.108Z",
"updatedAt": "2026-06-10T07:28:09.108Z"
},
{
"id": "CKOPA2qu9cEKy3LA",
"name": "calendar",
"createdAt": "2026-06-10T07:28:09.109Z",
"updatedAt": "2026-06-10T07:28:09.109Z"
}
],
"nodes": [
{
"id": "1e2380c6-4ebf-4529-9106-9fe8344479e6",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
784,
352
],
"parameters": {
"width": 980,
"height": 1264,
"content": "## Analyze Google Calendar patterns and send weekly optimization digest\n\nThis workflow automatically fetches your calendar data, detects hidden scheduling patterns, analyzes time allocation, and delivers actionable optimization suggestions to help you reclaim focus time and schedule smarter.\n\n### Who's it for\n\u2022 Knowledge workers overwhelmed by back-to-back meetings\n\u2022 Managers wanting to protect deep work time\n\u2022 Anyone tracking time allocation across projects or categories\n\u2022 Teams running weekly scheduling retrospectives\n\n### How it works / What it does\n1. Triggers on schedule (daily) or via webhook\n2. Fetches calendar events for the past 7 days + next 7 days\n3. Normalizes and filters events (excludes private/OOO)\n4. Detects patterns: meeting density, focus blocks, fragmentation\n5. Categorizes events: deep work, collaboration, admin, 1:1s\n6. AI generates personalized optimization recommendations\n7. Sends digest email + logs insights to Google Sheets\n\n### How to set up\n1. Import this workflow into n8n\n2. Configure credentials: Google Calendar OAuth2, Gmail, Google Sheets, OpenAI\n3. Update YOUR_SHEET_ID and your email in the relevant nodes\n4. Adjust category keywords in the Python classification node\n5. Activate the workflow\n\n### Requirements\n\u2022 Google Calendar OAuth2 credentials\n\u2022 Gmail credentials (or SMTP)\n\u2022 Google Sheets API access\n\u2022 OpenAI API key (GPT-4.1-mini)\n\n### How to customize\n\u2022 Modify Python keyword lists to match your event naming conventions\n\u2022 Change the analysis window (default: 7 days back, 7 days forward)\n\u2022 Add Slack notification node after the email send node\n\u2022 Adjust the AI prompt tone in the AI Insights node\n\n### Credential Setup Checklist\n\u2705 Google Calendar OAuth2\n\u2705 Gmail OAuth2 (for sending digest)\n\u2705 Google Sheets OAuth2\n\u2705 OpenAI API key\n\nReplace all `REPLACE_WITH_YOUR_*_CREDENTIAL_ID` placeholder strings with your actual n8n credential IDs.\n\n### Google Sheets Column Layout\nSet up your sheet with these headers in row 1:\n\n`Date | Health Score | Total Events | Meeting Hours | Focus Hours | Peak Hour | Heaviest Day | Max B2B Chain | Fragmentation | Category Breakdown | AI Insights`\n\n**Replace `YOUR_SHEET_ID`** in the Log to Google Sheets node with your actual Spreadsheet ID (found in the URL of your Google Sheet)."
},
"typeVersion": 1
},
{
"id": "bc75fc25-9144-46bd-bc9e-1e94dad38d0f",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1888,
592
],
"parameters": {
"color": 5,
"width": 780,
"height": 500,
"content": "## 1. Trigger & Calendar Fetch"
},
"typeVersion": 1
},
{
"id": "49b15e2d-ce3d-4c7b-b0d1-6d1b2cccaaf6",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2736,
544
],
"parameters": {
"color": 5,
"width": 840,
"height": 560,
"content": "## 2. Normalize, Filter & Classify Events"
},
"typeVersion": 1
},
{
"id": "3a701d70-df03-4e29-b45e-93661bccbc07",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
3632,
544
],
"parameters": {
"color": 5,
"width": 820,
"height": 784,
"content": "## 3. Pattern Detection & Metrics"
},
"typeVersion": 1
},
{
"id": "b7f8b07e-711e-4760-bff2-b27bf8767ace",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
4512,
560
],
"parameters": {
"color": 5,
"width": 900,
"height": 860,
"content": "## 4. AI Insights & Output Delivery"
},
"typeVersion": 1
},
{
"id": "5ae8300f-50f2-4b4b-919a-b867bd6ebe8d",
"name": "Daily Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
2048,
720
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 7 * * 1-5"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "93c3f767-0253-45f2-8929-22d7ba1a54fc",
"name": "Webhook - Manual Trigger",
"type": "n8n-nodes-base.webhook",
"position": [
2048,
896
],
"parameters": {
"path": "calendar-analytics-trigger",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 1.1
},
{
"id": "66e9c18d-e93d-45f6-8993-ea92161cd0e3",
"name": "Set Analysis Window",
"type": "n8n-nodes-base.set",
"position": [
2288,
800
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "windowDaysBack",
"type": "number",
"value": 7
},
{
"name": "windowDaysForward",
"type": "number",
"value": 7
},
{
"name": "timeMin",
"type": "string",
"value": "={{ new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString() }}"
},
{
"name": "timeMax",
"type": "string",
"value": "={{ new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }}"
},
{
"name": "runDate",
"type": "string",
"value": "={{ new Date().toISOString().split('T')[0] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "e3ddb76a-e151-428d-b3bc-0aba917ca2d8",
"name": "Fetch Google Calendar Events",
"type": "n8n-nodes-base.googleCalendar",
"position": [
2528,
800
],
"parameters": {
"options": {
"orderBy": "startTime",
"timeMax": "={{ $json.timeMax }}",
"timeMin": "={{ $json.timeMin }}",
"singleEvents": true
},
"calendar": {
"__rl": true,
"mode": "id",
"value": "=F3weGWAED"
},
"operation": "getAll",
"returnAll": true
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "1b7d76f8-87f2-4818-b620-7d5c2008a058",
"name": "Python - Normalize Events",
"type": "n8n-nodes-base.code",
"position": [
2976,
800
],
"parameters": {
"mode": "runOnceForEachItem",
"language": "pythonNative",
"pythonCode": "item = _input.item.json\n\n# Extract start/end (handle both dateTime and date-only events)\nstart_raw = (item.get('start') or {})\nend_raw = (item.get('end') or {})\n\nstart_str = start_raw.get('dateTime') or start_raw.get('date') or ''\nend_str = end_raw.get('dateTime') or end_raw.get('date') or ''\n\nis_all_day = 'dateTime' not in (item.get('start') or {})\n\n# Parse duration\nduration_minutes = 0\nif start_str and end_str and not is_all_day:\n from datetime import datetime\n fmt = '%Y-%m-%dT%H:%M:%S%z'\n try:\n s = datetime.fromisoformat(start_str)\n e = datetime.fromisoformat(end_str)\n duration_minutes = max(0, int((e - s).total_seconds() / 60))\n except Exception:\n duration_minutes = 0\n\n# Privacy / OOO check\nsummary = (item.get('summary') or '').strip()\nvis = (item.get('visibility') or '').lower()\nooo_terms = ['out of office', 'ooo', 'holiday', 'vacation', 'leave', 'unavailable']\nprivacy_flag = vis in ('private', 'confidential') or any(t in summary.lower() for t in ooo_terms)\n\n# Hour of day for peak-hour analysis\nhour_of_day = 0\nif start_str and not is_all_day:\n try:\n from datetime import datetime\n hour_of_day = datetime.fromisoformat(start_str).hour\n except Exception:\n hour_of_day = 0\n\n# Day of week\nday_name = ''\nif start_str and not is_all_day:\n try:\n from datetime import datetime\n day_name = datetime.fromisoformat(start_str).strftime('%A')\n except Exception:\n day_name = ''\n\nreturn {\n 'json': {\n 'eventId': item.get('id', ''),\n 'summary': summary,\n 'startTime': start_str,\n 'endTime': end_str,\n 'durationMinutes': duration_minutes,\n 'hourOfDay': hour_of_day,\n 'dayOfWeek': day_name,\n 'isAllDay': is_all_day,\n 'privacyFlag': privacy_flag,\n 'status': (item.get('status') or '').lower(),\n 'attendeeCount': len(item.get('attendees') or []),\n 'isRecurring': 'recurringEventId' in item,\n 'recurringId': item.get('recurringEventId', ''),\n 'location': item.get('location', ''),\n 'organizer': (item.get('organizer') or {}).get('email', '')\n }\n}"
},
"typeVersion": 2
},
{
"id": "39e4c671-cb5b-428d-9f58-1707f8cf07b4",
"name": "Filter Valid Events",
"type": "n8n-nodes-base.filter",
"position": [
3168,
800
],
"parameters": {
"options": {},
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "boolean",
"operation": "false"
},
"leftValue": "={{ $json.privacyFlag }}"
},
{
"operator": {
"type": "boolean",
"operation": "false"
},
"leftValue": "={{ $json.isAllDay }}"
},
{
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "cancelled"
},
{
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.durationMinutes }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3d8a5340-6fea-45ce-a357-5256bf1e5610",
"name": "Python - Classify Events",
"type": "n8n-nodes-base.code",
"position": [
3392,
800
],
"parameters": {
"mode": "runOnceForEachItem",
"language": "pythonNative",
"pythonCode": "item = _input.item.json\n\nsummary = (item.get('summary') or '').lower()\nattendees = item.get('attendeeCount', 0)\n\n# Keyword maps per category\nDEEP_WORK = ['focus', 'deep work', 'writing', 'coding', 'research', 'review', 'analysis', 'blocked', 'no meeting', 'heads down', 'design']\nCOLLABORATION = ['brainstorm', 'planning', 'sprint', 'standup', 'sync', 'team', 'workshop', 'kickoff', 'retrospective', 'demo', 'presentation', 'all hands']\nONE_ON_ONE = ['1:1', '1-1', 'one on one', 'one-on-one', 'catch up', 'check-in', 'checkin']\nADMIN = ['interview', 'onboarding', 'hr', 'finance', 'budget', 'admin', 'payroll', 'compliance', 'legal', 'contract']\nEXTERNAL = ['client', 'customer', 'sales', 'demo', 'prospect', 'vendor', 'partner', 'discovery']\nPERSONAL = ['lunch', 'break', 'gym', 'coffee', 'personal', 'family', 'doctor', 'appointment']\n\ndef classify(s, n_attendees):\n if any(k in s for k in PERSONAL):\n return 'personal'\n if any(k in s for k in DEEP_WORK):\n return 'deep_work'\n if any(k in s for k in ONE_ON_ONE) or n_attendees == 2:\n return 'one_on_one'\n if any(k in s for k in EXTERNAL):\n return 'external'\n if any(k in s for k in ADMIN):\n return 'admin'\n if any(k in s for k in COLLABORATION) or n_attendees >= 3:\n return 'collaboration'\n return 'uncategorized'\n\ncategory = classify(summary, attendees)\n\n# Is this a focus block (solo, 30+ min, no attendees)\nis_focus_block = (\n attendees == 0 and\n item.get('durationMinutes', 0) >= 30 and\n category in ('deep_work', 'uncategorized')\n)\n\nreturn {\n 'json': {\n **item,\n 'category': category,\n 'isFocusBlock': is_focus_block,\n 'isMeeting': attendees >= 2 or category in ('collaboration', 'one_on_one', 'external')\n }\n}"
},
"typeVersion": 2
},
{
"id": "e7ab1886-5371-47be-9cbd-e5c9fa95ee76",
"name": "Aggregate All Events",
"type": "n8n-nodes-base.merge",
"position": [
3712,
800
],
"parameters": {
"mode": "combine",
"options": {}
},
"typeVersion": 3
},
{
"id": "084c559b-467a-41e3-901e-960547f7e1b1",
"name": "Python - Pattern Detection Engine",
"type": "n8n-nodes-base.code",
"position": [
3968,
800
],
"parameters": {
"language": "pythonNative",
"pythonCode": "items = _input.all()\n\nimport json\nfrom collections import defaultdict\n\nevents = [i.json for i in items]\n\n# \u2500\u2500 Basic totals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ntotal_events = len(events)\nmeeting_minutes = sum(e.get('durationMinutes', 0) for e in events if e.get('isMeeting'))\nfocus_minutes = sum(e.get('durationMinutes', 0) for e in events if e.get('isFocusBlock'))\nscheduled_minutes = sum(e.get('durationMinutes', 0) for e in events)\n\n# \u2500\u2500 Category breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ncategory_minutes = defaultdict(int)\nfor e in events:\n category_minutes[e.get('category', 'uncategorized')] += e.get('durationMinutes', 0)\n\n# \u2500\u2500 Peak hour (most meetings) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nhour_counts = defaultdict(int)\nfor e in events:\n if e.get('isMeeting'):\n hour_counts[e.get('hourOfDay', 0)] += 1\n\npeak_hour = max(hour_counts, key=hour_counts.get) if hour_counts else 9\nquietest_hour = min(hour_counts, key=hour_counts.get) if hour_counts else 16\n\n# \u2500\u2500 Day of week load \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nday_minutes = defaultdict(int)\nfor e in events:\n if e.get('isMeeting'):\n day_minutes[e.get('dayOfWeek', 'Unknown')] += e.get('durationMinutes', 0)\n\nheaviest_day = max(day_minutes, key=day_minutes.get) if day_minutes else 'Unknown'\nlightest_day = min(day_minutes, key=day_minutes.get) if day_minutes else 'Unknown'\n\n# \u2500\u2500 Back-to-back detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfrom datetime import datetime\n\ndef parse_dt(s):\n try:\n return datetime.fromisoformat(s)\n except Exception:\n return None\n\nmeetings_sorted = sorted(\n [e for e in events if e.get('isMeeting') and e.get('startTime')],\n key=lambda e: e['startTime']\n)\n\nmax_b2b_chain = 0\ntotal_b2b = 0\ncurrent_chain = 1\n\nfor i in range(1, len(meetings_sorted)):\n prev_end = parse_dt(meetings_sorted[i-1].get('endTime', ''))\n curr_start = parse_dt(meetings_sorted[i].get('startTime', ''))\n if prev_end and curr_start:\n gap_minutes = (curr_start - prev_end).total_seconds() / 60\n if gap_minutes <= 10:\n current_chain += 1\n total_b2b += 1\n else:\n max_b2b_chain = max(max_b2b_chain, current_chain)\n current_chain = 1\n\nmax_b2b_chain = max(max_b2b_chain, current_chain)\n\n# \u2500\u2500 Fragmentation score (0-100) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# More short gaps between events = higher fragmentation\nshort_gaps = 0\nfor i in range(1, len(meetings_sorted)):\n prev_end = parse_dt(meetings_sorted[i-1].get('endTime', ''))\n curr_start = parse_dt(meetings_sorted[i].get('startTime', ''))\n if prev_end and curr_start:\n gap = (curr_start - prev_end).total_seconds() / 60\n if 0 < gap < 30:\n short_gaps += 1\n\nfragmentation_score = min(100, round((short_gaps / max(total_events, 1)) * 100))\n\n# \u2500\u2500 Free focus blocks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfree_blocks = []\nfor i in range(1, len(meetings_sorted)):\n prev_end = parse_dt(meetings_sorted[i-1].get('endTime', ''))\n curr_start = parse_dt(meetings_sorted[i].get('startTime', ''))\n if prev_end and curr_start:\n gap = (curr_start - prev_end).total_seconds() / 60\n if gap >= 60:\n free_blocks.append({\n 'start': meetings_sorted[i-1]['endTime'],\n 'end': meetings_sorted[i]['startTime'],\n 'gapMinutes': int(gap)\n })\n\nfree_blocks = sorted(free_blocks, key=lambda x: -x['gapMinutes'])[:5]\n\n# \u2500\u2500 Recurring meetings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nrecurring_counts = defaultdict(int)\nfor e in events:\n if e.get('isRecurring') and e.get('summary'):\n recurring_counts[e['summary']] += 1\n\ntop_recurring = sorted(recurring_counts.items(), key=lambda x: -x[1])[:5]\n\n# \u2500\u2500 Suboptimal flags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nflags = []\nif max_b2b_chain >= 3:\n flags.append(f'Long back-to-back chain detected: {max_b2b_chain} meetings in a row')\nif focus_minutes < 60:\n flags.append('Less than 1 hour of protected focus time in the window')\nif meeting_minutes > scheduled_minutes * 0.7:\n flags.append('Over 70% of scheduled time is in meetings')\nif fragmentation_score > 60:\n flags.append(f'High calendar fragmentation ({fragmentation_score}/100)')\nif hour_counts.get(12, 0) >= 3:\n flags.append('Lunch hour frequently consumed by meetings')\n\nreturn {\n 'json': {\n 'runDate': (events[0].get('startTime', '')[:10] if events else ''),\n 'totalEvents': total_events,\n 'totalMeetingMinutes': meeting_minutes,\n 'totalFocusMinutes': focus_minutes,\n 'totalScheduledMinutes': scheduled_minutes,\n 'peakMeetingHour': peak_hour,\n 'quietestHour': quietest_hour,\n 'heaviestDay': heaviest_day,\n 'lightestDay': lightest_day,\n 'maxBackToBackChain': max_b2b_chain,\n 'totalBackToBackInstances': total_b2b,\n 'fragmentationScore': fragmentation_score,\n 'categoryMinutesJSON': json.dumps(dict(category_minutes)),\n 'suboptimalFlagsJSON': json.dumps(flags),\n 'freeBlocksJSON': json.dumps(free_blocks),\n 'recurringMeetingsJSON': json.dumps(top_recurring)\n }\n}"
},
"typeVersion": 2
},
{
"id": "10df990d-5881-4e9c-82fa-f6c111018f15",
"name": "AI - Generate Optimization Insights",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
4176,
800
],
"parameters": {
"text": "=You are an expert productivity coach and calendar optimization specialist. Analyze the following calendar metrics and generate a concise, actionable weekly optimization report.\n\n## Calendar Metrics Summary\n- Analysis run date: {{ $json.runDate }}\n- Total events analyzed: {{ $json.totalEvents }}\n- Total time in meetings: {{ Math.round($json.totalMeetingMinutes / 60 * 10) / 10 }} hours\n- Total protected focus time: {{ Math.round($json.totalFocusMinutes / 60 * 10) / 10 }} hours\n- Total scheduled time: {{ Math.round($json.totalScheduledMinutes / 60 * 10) / 10 }} hours\n\n## Meeting Density\n- Peak meeting hour: {{ $json.peakMeetingHour }}:00\n- Quietest available hour: {{ $json.quietestHour }}:00\n- Heaviest meeting day: {{ $json.heaviestDay }}\n- Lightest meeting day: {{ $json.lightestDay }}\n\n## Fragmentation & Back-to-Back\n- Longest back-to-back chain: {{ $json.maxBackToBackChain }} meetings\n- Total back-to-back instances: {{ $json.totalBackToBackInstances }}\n- Calendar fragmentation score: {{ $json.fragmentationScore }}/100 (higher = more fragmented)\n\n## Category Breakdown (minutes)\n{{ $json.categoryMinutesJSON }}\n\n## Detected Suboptimal Patterns\n{{ $json.suboptimalFlagsJSON }}\n\n## Largest Free Focus Blocks Available\n{{ $json.freeBlocksJSON }}\n\n## Top Recurring Meetings\n{{ $json.recurringMeetingsJSON }}\n\n---\n\nPlease provide:\n1. **Executive Summary** (2-3 sentences on overall calendar health)\n2. **Top 3 Optimization Recommendations** (specific, actionable, prioritized)\n3. **Best Focus Windows This Week** (specific time slots to protect based on the free blocks above)\n4. **One Quick Win** (something that can be changed immediately with minimal effort)\n\nBe direct, specific, and limit the full response to under 350 words. Use a friendly but professional tone.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "644e9264-2cd0-4d70-9f53-1b6be628b26e",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
4240,
1040
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "3c149199-c958-45c4-8045-4da669e10ff2",
"name": "JS - Format Final Report",
"type": "n8n-nodes-base.code",
"position": [
4672,
800
],
"parameters": {
"jsCode": "// Merge AI insights with pattern metrics and format final output payload\nconst aiItem = $input.all()[0].json;\nconst metricsRaw = $('Python - Pattern Detection Engine').all()[0].json;\n\nconst insightText = aiItem.output || aiItem.response || aiItem.text || 'No insights generated.';\n\n// Compute a simple health score 0-100\nconst focusRatio = metricsRaw.totalFocusMinutes / Math.max(metricsRaw.totalScheduledMinutes, 1);\nconst b2bPenalty = Math.min(metricsRaw.maxBackToBackChain * 8, 40);\nconst fragPenalty = Math.round(metricsRaw.fragmentationScore * 0.3);\nconst healthScore = Math.max(0, Math.min(100, Math.round(50 + focusRatio * 50 - b2bPenalty - fragPenalty)));\n\nconst formattedDate = new Date().toISOString().split('T')[0];\n\nreturn [{\n json: {\n reportDate: formattedDate,\n healthScore: healthScore,\n totalEvents: metricsRaw.totalEvents,\n totalMeetingHours: +(metricsRaw.totalMeetingMinutes / 60).toFixed(1),\n totalFocusHours: +(metricsRaw.totalFocusMinutes / 60).toFixed(1),\n peakMeetingHour: metricsRaw.peakMeetingHour,\n heaviestDay: metricsRaw.heaviestDay,\n maxBackToBackChain: metricsRaw.maxBackToBackChain,\n fragmentationScore: metricsRaw.fragmentationScore,\n categoryMinutesJSON: metricsRaw.categoryMinutesJSON,\n suboptimalFlags: metricsRaw.suboptimalFlagsJSON,\n freeBlocks: metricsRaw.freeBlocksJSON,\n aiInsights: insightText,\n emailSubject: `\ud83d\udcc5 Calendar Optimization Report \u2014 ${formattedDate} | Health Score: ${healthScore}/100`\n }\n}];"
},
"typeVersion": 2
},
{
"id": "6cbebaf5-7651-4b18-b226-38012a1183da",
"name": "Send Digest Email",
"type": "n8n-nodes-base.gmail",
"position": [
5056,
704
],
"parameters": {
"sendTo": "your-email@gmail.com",
"message": "=<html><body style=\"font-family: Arial, sans-serif; max-width: 680px; margin: auto; color: #222;\">\n\n<h2 style=\"color:#1a73e8;\">\ud83d\udcc5 Weekly Calendar Optimization Report</h2>\n<p style=\"color:#555;\">Generated: {{ $json.reportDate }}</p>\n\n<div style=\"background:#f0f4ff;border-left:4px solid #1a73e8;padding:16px;border-radius:4px;margin-bottom:20px;\">\n <strong>Calendar Health Score: <span style=\"font-size:1.4em;color:#1a73e8;\">{{ $json.healthScore }}/100</span></strong>\n</div>\n\n<h3>\ud83d\udcca Quick Stats</h3>\n<table style=\"width:100%;border-collapse:collapse;\">\n <tr style=\"background:#f8f8f8;\"><td style=\"padding:8px;border:1px solid #ddd;\">Total Events</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.totalEvents }}</strong></td></tr>\n <tr><td style=\"padding:8px;border:1px solid #ddd;\">Meeting Hours</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.totalMeetingHours }}h</strong></td></tr>\n <tr style=\"background:#f8f8f8;\"><td style=\"padding:8px;border:1px solid #ddd;\">Protected Focus Time</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.totalFocusHours }}h</strong></td></tr>\n <tr><td style=\"padding:8px;border:1px solid #ddd;\">Peak Meeting Hour</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.peakMeetingHour }}:00</strong></td></tr>\n <tr style=\"background:#f8f8f8;\"><td style=\"padding:8px;border:1px solid #ddd;\">Heaviest Day</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.heaviestDay }}</strong></td></tr>\n <tr><td style=\"padding:8px;border:1px solid #ddd;\">Longest Back-to-Back Chain</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.maxBackToBackChain }} meetings</strong></td></tr>\n <tr style=\"background:#f8f8f8;\"><td style=\"padding:8px;border:1px solid #ddd;\">Fragmentation Score</td><td style=\"padding:8px;border:1px solid #ddd;\"><strong>{{ $json.fragmentationScore }}/100</strong></td></tr>\n</table>\n\n<h3>\ud83e\udd16 AI Optimization Insights</h3>\n<div style=\"background:#fafafa;border:1px solid #ddd;padding:16px;border-radius:4px;white-space:pre-wrap;\">{{ $json.aiInsights }}</div>\n\n<h3>\u26a0\ufe0f Detected Issues</h3>\n<div style=\"background:#fff8e1;border:1px solid #ffc107;padding:12px;border-radius:4px;\">{{ $json.suboptimalFlags }}</div>\n\n<h3>\ud83d\udfe2 Available Focus Blocks</h3>\n<div style=\"background:#e8f5e9;border:1px solid #4caf50;padding:12px;border-radius:4px;\">{{ $json.freeBlocks }}</div>\n\n<hr style=\"margin:24px 0;border:none;border-top:1px solid #eee;\">\n<p style=\"color:#999;font-size:0.85em;\">Sent automatically by your n8n Calendar Analytics workflow. <a href=\"https://n8n.io\">Powered by n8n</a></p>\n</body></html>",
"options": {
"appendAttribution": false
},
"subject": "={{ $json.emailSubject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "2bf358bb-5757-47b2-b5a5-1c682ab7fa98",
"name": "Log to Google Sheets",
"type": "n8n-nodes-base.httpRequest",
"position": [
5056,
896
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/CalendarInsights!A1:append?valueInputOption=USER_ENTERED",
"method": "POST",
"options": {},
"jsonBody": "={\n \"values\": [[\n \"{{ $json.reportDate }}\",\n {{ $json.healthScore }},\n {{ $json.totalEvents }},\n {{ $json.totalMeetingHours }},\n {{ $json.totalFocusHours }},\n {{ $json.peakMeetingHour }},\n \"{{ $json.heaviestDay }}\",\n {{ $json.maxBackToBackChain }},\n {{ $json.fragmentationScore }},\n \"{{ $json.categoryMinutesJSON.replace(/\"/g, '\"\"') }}\",\n \"{{ $json.aiInsights.replace(/\"/g, '\"\"').replace(/\\n/g, ' ') }}\"\n ]]\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "ae1dc34e-5170-4a4c-9708-aea2c8e2269d",
"name": "Webhook Response",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
5056,
1072
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ JSON.stringify({ status: 'success', reportDate: $('JS - Format Final Report').first().json.reportDate, healthScore: $('JS - Format Final Report').first().json.healthScore }) }}"
},
"typeVersion": 1
},
{
"id": "f750c011-bccb-47c7-9036-e262111bb776",
"name": "Wait For Result",
"type": "n8n-nodes-base.wait",
"position": [
2768,
800
],
"parameters": {},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "4a8882af-6094-49e3-ba55-d8a57bf9ac8d",
"connections": {
"Wait For Result": {
"main": [
[
{
"node": "Python - Normalize Events",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "AI - Generate Optimization Insights",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Filter Valid Events": {
"main": [
[
{
"node": "Python - Classify Events",
"type": "main",
"index": 0
}
]
]
},
"Set Analysis Window": {
"main": [
[
{
"node": "Fetch Google Calendar Events",
"type": "main",
"index": 0
}
]
]
},
"Aggregate All Events": {
"main": [
[
{
"node": "Python - Pattern Detection Engine",
"type": "main",
"index": 0
}
]
]
},
"Daily Schedule Trigger": {
"main": [
[
{
"node": "Set Analysis Window",
"type": "main",
"index": 0
}
]
]
},
"JS - Format Final Report": {
"main": [
[
{
"node": "Send Digest Email",
"type": "main",
"index": 0
},
{
"node": "Log to Google Sheets",
"type": "main",
"index": 0
},
{
"node": "Webhook Response",
"type": "main",
"index": 0
}
]
]
},
"Python - Classify Events": {
"main": [
[
{
"node": "Aggregate All Events",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Manual Trigger": {
"main": [
[
{
"node": "Set Analysis Window",
"type": "main",
"index": 0
}
]
]
},
"Python - Normalize Events": {
"main": [
[
{
"node": "Filter Valid Events",
"type": "main",
"index": 0
}
]
]
},
"Fetch Google Calendar Events": {
"main": [
[
{
"node": "Wait For Result",
"type": "main",
"index": 0
}
]
]
},
"Python - Pattern Detection Engine": {
"main": [
[
{
"node": "AI - Generate Optimization Insights",
"type": "main",
"index": 0
}
]
]
},
"AI - Generate Optimization Insights": {
"main": [
[
{
"node": "JS - Format Final Report",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleCalendarOAuth2ApigoogleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs on a weekday schedule or via webhook, pulls events from Google Calendar, analyzes meeting and focus-time patterns, generates optimization advice with OpenAI, then emails a digest through Gmail and appends the results to Google Sheets. Runs every weekday…
Source: https://n8n.io/workflows/16230/ — 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 automated disaster response workflow streamlines emergency management by monitoring multiple alert sources and coordinating property protection teams. Designed for property managers, insurance co
Author: Hyrum Hurst, AI Automation Engineer at QuarterSmart Contact: hyrum@quartersmart.com
This n8n automation workflow automates the creation, scripting, production, and posting of YouTube videos. It leverages AI (OpenAI), image generation (PIAPI), video rendering (Shotstack), and platform
Transform your salon/service business with this streamlined WhatsApp automation system featuring Claude integration, zero-setup database management, and intelligent conversation handling. Claude MCP I
Created by: Peyton Leveillee Last updated: October 2025