This workflow corresponds to n8n.io template #13993 — we link there as the canonical source.
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": "7sF6TGsrR5dlGVAx",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Weekly GA4 Analytics Report For Website/App",
"tags": [],
"nodes": [
{
"id": "242dcef9-b89b-4d05-b9dd-616c8f51942c",
"name": "GA4 - Overview Current Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
-464
],
"parameters": {
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{},
{
"listName": "sessions"
},
{
"name": "bounce_rate",
"listName": "custom",
"expression": "bounceRate"
},
{
"name": "average_session_dur",
"listName": "custom",
"expression": "averageSessionDuration"
},
{
"name": "new_users",
"listName": "custom",
"expression": "newUsers"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "eb6868d0-0dab-49ab-99ba-b34108d16a17",
"name": "GA4 - Overview Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
-464
],
"parameters": {
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{},
{
"listName": "sessions"
},
{
"name": "bounce_rate",
"listName": "custom",
"expression": "bounceRate"
},
{
"name": "average_session_dur",
"listName": "custom",
"expression": "averageSessionDuration"
},
{
"name": "new_users",
"listName": "custom",
"expression": "newUsers"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "dca8f70f-f4ad-439a-b8b8-e9395d5b540e",
"name": "GA4 - Top 5 Pages",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
-272
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"name": "screen_page_view",
"listName": "custom",
"expression": "screenPageViews"
},
{
"name": "average_session_duration",
"listName": "custom",
"expression": "averageSessionDuration"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "unifiedScreenName",
"listName": "other"
}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "85950514-c48e-4cab-b3c5-159ecc0d7d4a",
"name": "GA4 - Top 5 Referrals",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
-80
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "sessionSourceMedium",
"listName": "other"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "0d766056-e67f-4827-838c-e6fe339231d9",
"name": "GA4 - Top 5 Events",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
112
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"name": "event_count",
"listName": "custom",
"expression": "eventCount"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "eventName",
"listName": "other"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "=event_count"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "9f0dabdd-36c0-41d3-9052-b9d2cdf8ea85",
"name": "GA4 - Top 5 Countries",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
304
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"listName": "country"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "1c982d79-9f8a-47c4-bb35-1b210abb1524",
"name": "GA4 - Device Breakdown",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
496
],
"parameters": {
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"listName": "deviceCategory"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "7de9e216-c86a-44b3-9ae5-1872e44e264a",
"name": "New vs Returning Users Breakdown",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-544,
688
],
"parameters": {
"endDate": "={{$now.minus({days: 1}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 7}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
},
{}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "newVsReturning",
"listName": "other"
}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "0ea9bb61-586a-4d25-8a15-c7b875464470",
"name": "Weekly Monday Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-976,
112
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1
],
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.3
},
{
"id": "43f5b7c2-562e-4003-9517-7022ab148529",
"name": "Wait for All GA4 Data",
"type": "n8n-nodes-base.merge",
"position": [
64,
32
],
"parameters": {
"mode": "combine",
"options": {
"includeUnpaired": true
},
"combineBy": "combineByPosition",
"numberInputs": 7
},
"typeVersion": 3.2
},
{
"id": "404f04c1-57ff-456e-917e-62ee967655be",
"name": "Generate AI Summary",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
368,
112
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-3.1-flash-lite-preview",
"cachedResultName": "models/gemini-3.1-flash-lite-preview"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a professional digital analytics consultant. Analyze the full week-over-week data below and write a concise executive summary.\n\n=== OVERVIEW ===\nTHIS WEEK \u2014 Users: {{$('GA4 - Overview Current Week').first().json.totalUsers}} | Sessions: {{$('GA4 - Overview Current Week').first().json.sessions}} | New Users: {{$('GA4 - Overview Current Week').first().json.new_users}} | Bounce Rate: {{$('GA4 - Overview Current Week').first().json.bounce_rate}} | Avg Duration (sec): {{$('GA4 - Overview Current Week').first().json.average_session_dur}}\nPREV WEEK \u2014 Users: {{$('GA4 - Overview Previous Week').first().json.totalUsers}} | Sessions: {{$('GA4 - Overview Previous Week').first().json.sessions}} | New Users: {{$('GA4 - Overview Previous Week').first().json.new_users}} | Bounce Rate: {{$('GA4 - Overview Previous Week').first().json.bounce_rate}} | Avg Duration (sec): {{$('GA4 - Overview Previous Week').first().json.average_session_dur}}\n\n=== TOP SCREENS ===\nTHIS WEEK: {{$('GA4 - Top 5 Pages').all().map((i,idx) => `${idx+1}. ${i.json.unifiedScreenName} (${i.json.screen_page_view} views)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Pages Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.unifiedScreenName} (${i.json.screen_page_view} views)`).join(' | ')}}\n\n=== TRAFFIC SOURCES ===\nTHIS WEEK: {{$('GA4 - Top 5 Referrals').all().map((i,idx) => `${idx+1}. ${i.json.sessionSourceMedium} (${i.json.sessions} sessions)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Referrals Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.sessionSourceMedium} (${i.json.sessions} sessions)`).join(' | ')}}\n\n=== TOP EVENTS ===\nTHIS WEEK: {{$('GA4 - Top 5 Events').all().map((i,idx) => `${idx+1}. ${i.json.eventName} (${i.json.event_count})`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Events Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.eventName} (${i.json.event_count})`).join(' | ')}}\n\n=== TOP COUNTRIES ===\nTHIS WEEK: {{$('GA4 - Top 5 Countries').all().map((i,idx) => `${idx+1}. ${i.json.country} (${i.json.sessions} sessions)`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Top 5 Countries Previous Week').all().map((i,idx) => `${idx+1}. ${i.json.country} (${i.json.sessions} sessions)`).join(' | ')}}\n\n=== DEVICES ===\nTHIS WEEK: {{$('GA4 - Device Breakdown').all().map(i => `${i.json.deviceCategory}: ${i.json.sessions}`).join(' | ')}}\nPREV WEEK: {{$('GA4 - Device Breakdown Previous Week').all().map(i => `${i.json.deviceCategory}: ${i.json.sessions}`).join(' | ')}}\n\n=== NEW VS RETURNING ===\nTHIS WEEK: {{$('New vs Returning Users Breakdown').all().map(i => `${i.json.newVsReturning}: ${i.json.totalUsers} users`).join(' | ')}}\nPREV WEEK: {{$('GA4 - New vs Returning Previous Week').all().map(i => `${i.json.newVsReturning}: ${i.json.totalUsers} users`).join(' | ')}}\n\n=== END OF DATA ===\n\nWrite exactly 3 to 5 bullet points summarizing the most important trends and changes. If a country, screen, or source appears in one week but not the other, treat it as entered or dropped from the top 5.\n\nRules:\n- Start each bullet with a dash (-)\n- Single sentence per bullet, maximum 25 words\n- Reference specific numbers and week-over-week changes\n- No markdown, no bold, no headers\n- Do not start any bullet with \"Based on the data\""
}
]
},
"builtInTools": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 1.1
},
{
"id": "b173f712-0bfe-4ec1-8ad4-ef011a7bb3a1",
"name": "Build Report & Email HTML",
"type": "n8n-nodes-base.code",
"position": [
864,
112
],
"parameters": {
"jsCode": "// ============================================================\n// GA4 WEEKLY REPORT v9 \u2014 ALL ALIGNMENT FIXED\n// AppStoneLab Technologies\n// ============================================================\n\n// \u2500\u2500 1. PULL DATA \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\n\nconst currentWeek = $('GA4 - Overview Current Week').first().json;\nconst previousWeek = $('GA4 - Overview Previous Week').first().json;\n\nconst pages = $('GA4 - Top 5 Pages').all()\n .map(i => i.json)\n .map(p => ({ ...p, unifiedScreenName: (!p.unifiedScreenName || p.unifiedScreenName === '') ? '(empty)' : p.unifiedScreenName }))\n .slice(0, 5);\n\nconst pagesPrev = $('GA4 - Top 5 Pages Previous Week').all()\n .map(i => i.json)\n .map(p => ({ ...p, unifiedScreenName: (!p.unifiedScreenName || p.unifiedScreenName === '') ? '(empty)' : p.unifiedScreenName }));\n\nconst referrals = $('GA4 - Top 5 Referrals').all()\n .map(i => i.json)\n .map(r => ({ ...r, sessionSourceMedium: r.sessionSourceMedium === '(direct) / (none)' ? 'Direct / App Open' : (r.sessionSourceMedium || 'Direct / App Open') }));\n\nconst referralsPrev = $('GA4 - Top 5 Referrals Previous Week').all()\n .map(i => i.json)\n .map(r => ({ ...r, sessionSourceMedium: r.sessionSourceMedium === '(direct) / (none)' ? 'Direct / App Open' : (r.sessionSourceMedium || 'Direct / App Open') }));\n\nconst countries = $('GA4 - Top 5 Countries').all().map(i => i.json);\nconst countriesPrev = $('GA4 - Top 5 Countries Previous Week').all().map(i => i.json);\nconst devices = $('GA4 - Device Breakdown').all().map(i => i.json);\nconst devicesPrev = $('GA4 - Device Breakdown Previous Week').all().map(i => i.json);\nconst newVsRet = $('New vs Returning Users Breakdown').all().map(i => i.json);\nconst newVsRetPrev = $('GA4 - New vs Returning Previous Week').all().map(i => i.json);\n\nconst EXCLUDE_EVENTS = ['first_visit','first_open','page_view','app_remove','os_update','app_update','app_store_refund'];\nconst events = $('GA4 - Top 5 Events').all().map(i => i.json).filter(e => !EXCLUDE_EVENTS.includes(e.eventName)).slice(0, 5);\nconst eventsPrev = $('GA4 - Top 5 Events Previous Week').all().map(i => i.json).filter(e => !EXCLUDE_EVENTS.includes(e.eventName));\n\n// Gemini\nlet aiSummary = '';\ntry {\n const g = $('Generate AI Summary').first().json;\n aiSummary = g.content?.parts?.[0]?.text || g.text || g.output || g.response || g.message?.content || g.candidates?.[0]?.content?.parts?.[0]?.text || '';\n} catch(e) { aiSummary = ''; }\naiSummary = typeof aiSummary === 'string' ? aiSummary : '';\n\nlet formattedAiSummary = '';\nif (aiSummary.trim().length > 0) {\n const lines = aiSummary.split('\\n').map(l => l.trim()).filter(l => l.length > 0);\n const items = lines.map(l => `<li style=\"margin-bottom:10px;padding-left:4px;\">${l.replace(/^[\\*\\-\u2022]\\s*/, '')}</li>`).join('');\n formattedAiSummary = `<ul style=\"margin:0;padding-left:18px;font-family:Georgia,serif;font-size:13.5px;color:#3D3530;line-height:1.7;\">${items}</ul>`;\n}\n\n// \u2500\u2500 2. HELPERS \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\u2500\n\nfunction wowChange(current, previous) {\n const c = parseFloat(current), p = parseFloat(previous);\n if (!p || p === 0) return { text: 'N/A', color: '#9CA3AF', arrow: '\u2014', pill: '#F3F4F6', pillText: '#6B7280' };\n const pct = ((c - p) / p) * 100;\n const up = pct >= 0;\n return { text: (up ? '+' : '') + pct.toFixed(1) + '%', color: up ? '#2D6A4F' : '#B91C1C', arrow: up ? '\u2191' : '\u2193', pill: up ? '#ECFDF5' : '#FEF2F2', pillText: up ? '#2D6A4F' : '#B91C1C' };\n}\n\nfunction wowChangeBounce(current, previous) {\n const c = parseFloat(current), p = parseFloat(previous);\n if (!p || p === 0) return { text: 'N/A', color: '#9CA3AF', arrow: '\u2014', pill: '#F3F4F6', pillText: '#6B7280' };\n const pct = ((c - p) / p) * 100;\n const isGood = pct <= 0;\n return { text: (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%', color: isGood ? '#2D6A4F' : '#B91C1C', arrow: isGood ? '\u2193' : '\u2191', pill: isGood ? '#ECFDF5' : '#FEF2F2', pillText: isGood ? '#2D6A4F' : '#B91C1C' };\n}\n\nfunction listWoWWithPrev(currItem, prevItems, nameField, valueField) {\n const prev = prevItems.find(p => p[nameField] === currItem[nameField]);\n const currVal = parseInt(currItem[valueField] || 0);\n if (!prev) return { wow: { text: 'New', arrow: '\u2605', pill: '#FBF5EB', pillText: '#C9A96E' }, prevValue: null };\n const prevVal = parseInt(prev[valueField] || 0);\n if (!prevVal || prevVal === 0) return { wow: { text: 'New', arrow: '\u2605', pill: '#FBF5EB', pillText: '#C9A96E' }, prevValue: null };\n const pct = ((currVal - prevVal) / prevVal) * 100;\n const up = pct >= 0;\n return {\n wow: { text: (up ? '+' : '') + pct.toFixed(1) + '%', arrow: up ? '\u2191' : '\u2193', pill: up ? '#ECFDF5' : '#FEF2F2', pillText: up ? '#2D6A4F' : '#B91C1C' },\n prevValue: prevVal,\n };\n}\n\nfunction formatDuration(s) {\n const sec = parseFloat(s);\n if (isNaN(sec)) return '0s';\n const m = Math.floor(sec / 60), r = Math.round(sec % 60);\n return m > 0 ? `${m}m ${r}s` : `${r}s`;\n}\nfunction fmt(n) { const num = parseInt(n); return isNaN(num) ? '0' : num.toLocaleString('en-IN'); }\nfunction fmtPct(n) { return (parseFloat(n) * 100).toFixed(1) + '%'; }\n\n// \u2500\u2500 3. WoW \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\u2500\u2500\u2500\u2500\u2500\n\nconst wow = {\n totalUsers: wowChange(currentWeek.totalUsers, previousWeek.totalUsers),\n sessions: wowChange(currentWeek.sessions, previousWeek.sessions),\n newUsers: wowChange(currentWeek.new_users, previousWeek.new_users),\n bounceRate: wowChangeBounce(currentWeek.bounce_rate, previousWeek.bounce_rate),\n avgDuration: wowChange(currentWeek.average_session_dur, previousWeek.average_session_dur),\n};\n\nconst newUsersRow = newVsRet.find(r => r.newVsReturning === 'new') || { totalUsers: 0 };\nconst returningRow = newVsRet.find(r => r.newVsReturning === 'returning') || { totalUsers: 0 };\nconst newUsersPrevRow = newVsRetPrev.find(r => r.newVsReturning === 'new') || { totalUsers: 0 };\nconst retUsersPrevRow = newVsRetPrev.find(r => r.newVsReturning === 'returning') || { totalUsers: 0 };\nconst totalNvR = (parseInt(newUsersRow.totalUsers) + parseInt(returningRow.totalUsers)) || 1;\nconst wowNewUsers = wowChange(newUsersRow.totalUsers, newUsersPrevRow.totalUsers);\nconst wowRetUsers = wowChange(returningRow.totalUsers, retUsersPrevRow.totalUsers);\nconst totalDeviceSessions = devices.reduce((s, d) => s + parseInt(d.sessions || 0), 0) || 1;\n\nconst startLabel = $now.minus({days: 7}).toFormat('MMM dd, yyyy');\nconst endLabel = $now.minus({days: 1}).toFormat('MMM dd, yyyy');\nconst prevStartLabel = $now.minus({days: 14}).toFormat('MMM dd');\nconst prevEndLabel = $now.minus({days: 8}).toFormat('MMM dd');\n\n// \u2500\u2500 4. DESIGN TOKENS \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\n\nconst C = {\n bg: '#F7F5F2', card: '#FFFFFF', headerBg: '#1C1C1C', headerSub: '#A89880',\n gold: '#C9A96E', sectionText: '#1C1C1C', label: '#6B6560', value: '#1C1C1C',\n prevValue: '#B0A89E', rowAlt: '#FDFCFB', rowBase: '#FFFFFF', divider: '#EDE8E2',\n footerBg: '#1C1C1C', footerText: '#6B6560',\n};\n\n// \u2500\u2500 5. HTML COMPONENTS \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\n\nconst sectionGap = `<tr><td colspan=\"5\" style=\"padding:8px 0;background:${C.bg};font-size:0;line-height:0;\"> </td></tr>`;\n\nconst sectionHeader = (title, subtitle) => `\n<tr><td colspan=\"5\" style=\"padding:0;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr><td style=\"padding:18px 24px 13px;border-bottom:2px solid ${C.gold};background:${C.card};\">\n <span style=\"font-family:Georgia,serif;font-size:15px;font-weight:bold;color:${C.sectionText};letter-spacing:0.3px;\">${title}</span>\n ${subtitle ? `<span style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-left:10px;text-transform:uppercase;letter-spacing:0.8px;\">${subtitle}</span>` : ''}\n </td></tr>\n </table>\n</td></tr>`;\n\n// colHeaders accepts array of {label, align} \u2014 aligns header text to match data cells\nconst colHeaders = (cols) => `\n<tr style=\"background:#F9F7F4;\">\n ${cols.map((c, i) => {\n const align = c.align || 'left';\n const padLeft = (align === 'left' && i === 0) ? '24px' : '12px';\n const padRight = align === 'right' ? '20px' : '12px';\n return `<td style=\"font-family:Arial,sans-serif;font-size:10px;font-weight:bold;color:${C.label};\n padding:10px ${padRight} 10px ${padLeft};border-bottom:1px solid ${C.divider};\n text-transform:uppercase;letter-spacing:1px;text-align:${align};white-space:nowrap;\">${c.label}</td>`;\n }).join('')}\n</tr>`;\n\n// Change cell: \"was X\" + pill both right-aligned, stacked, in a right-aligned block\n// Both elements are display:block and text-align:right so they stack flush right\nfunction changeCell(wowObj, prevValue) {\n const wasLine = prevValue !== null\n ? `<span style=\"display:block;font-family:Arial,sans-serif;font-size:10px;color:#B0A89E;text-align:right;margin-bottom:4px;\">was ${fmt(prevValue)}</span>`\n : '';\n const pillLine = `<span style=\"display:block;text-align:right;\">\n <span style=\"display:inline-block;background:${wowObj.pill};border-radius:12px;padding:3px 8px;\n font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wowObj.pillText};white-space:nowrap;\">\n ${wowObj.arrow} ${wowObj.text}\n </span>\n </span>`;\n return `<td style=\"padding:13px 20px 13px 12px;border-bottom:1px solid ${C.divider};vertical-align:middle;\">${wasLine}${pillLine}</td>`;\n}\n\n// Inline pill (no \"was\" \u2014 used in overview where Last Week column already exists)\nfunction pillOnly(wowObj) {\n return `<span style=\"display:inline-block;background:${wowObj.pill};border-radius:12px;padding:3px 8px;\n font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wowObj.pillText};white-space:nowrap;\">\n ${wowObj.arrow} ${wowObj.text}\n </span>`;\n}\n\n// Overview row: label(left) / this week(right) / last week(right) / pill(right)\nconst overviewRow = (label, curr, prev, wowObj, isAlt) => `\n<tr style=\"background:${isAlt ? C.rowAlt : C.rowBase};\">\n <td style=\"font-family:Arial,sans-serif;font-size:13px;color:${C.label};padding:14px 12px 14px 24px;border-bottom:1px solid ${C.divider};width:34%;text-align:left;\">${label}</td>\n <td style=\"font-family:Georgia,serif;font-size:15px;font-weight:bold;color:${C.value};padding:14px 12px;border-bottom:1px solid ${C.divider};width:18%;text-align:right;\">${curr}</td>\n <td style=\"font-family:Arial,sans-serif;font-size:13px;color:${C.prevValue};padding:14px 12px;border-bottom:1px solid ${C.divider};width:18%;text-align:right;\">${prev}</td>\n <td style=\"padding:14px 20px 14px 12px;border-bottom:1px solid ${C.divider};width:30%;text-align:right;\">${pillOnly(wowObj)}</td>\n</tr>`;\n\n// List row: rank(center) / name(left) / value(right) / change cell(right-stacked)\nconst listRowWoW = (rank, name, value, wowObj, prevValue, isAlt) => `\n<tr style=\"background:${isAlt ? C.rowAlt : C.rowBase};\">\n <td style=\"font-family:Arial,sans-serif;font-size:12px;color:${C.gold};font-weight:bold;padding:13px 0 13px 24px;border-bottom:1px solid ${C.divider};width:6%;text-align:center;\">${rank}</td>\n <td style=\"font-family:Arial,sans-serif;font-size:13px;color:${C.value};padding:13px 12px;border-bottom:1px solid ${C.divider};text-align:left;\">${name}</td>\n <td style=\"font-family:Georgia,serif;font-size:14px;font-weight:bold;color:${C.value};padding:13px 12px;border-bottom:1px solid ${C.divider};text-align:right;white-space:nowrap;\">${value}</td>\n ${changeCell(wowObj, prevValue)}\n</tr>`;\n\n// Bar row: label(left) / bar / sessions n+%(right) / change cell(right-stacked)\nconst barRowWoW = (label, value, total, barColor, wowObj, prevValue, isAlt) => {\n const pct = ((parseInt(value || 0) / total) * 100).toFixed(1);\n const barWidth = Math.min(99, Math.max(1, Math.round(parseFloat(pct))));\n return `\n<tr style=\"background:${isAlt ? C.rowAlt : C.rowBase};\">\n <td style=\"font-family:Arial,sans-serif;font-size:13px;color:${C.value};padding:14px 24px 14px 24px;border-bottom:1px solid ${C.divider};width:22%;text-align:left;\">${label}</td>\n <td style=\"padding:14px 12px;border-bottom:1px solid ${C.divider};width:26%;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n <td style=\"width:${barWidth}%;background:${barColor};height:6px;border-radius:3px 0 0 3px;font-size:0;line-height:0;\"></td>\n <td style=\"width:${100-barWidth}%;background:#EDE8E2;height:6px;border-radius:0 3px 3px 0;font-size:0;line-height:0;\"></td>\n </tr></table>\n </td>\n <td style=\"font-family:Georgia,serif;font-size:14px;font-weight:bold;color:${C.value};padding:14px 12px;border-bottom:1px solid ${C.divider};text-align:right;white-space:nowrap;width:24%;\">\n ${fmt(value)} <span style=\"font-family:Arial,sans-serif;font-size:11px;color:${C.prevValue};font-weight:normal;\">${pct}%</span>\n </td>\n ${changeCell(wowObj, prevValue)}\n</tr>`;\n};\n\n// \u2500\u2500 6. BUILD ROWS \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\n\nconst deviceColors = { mobile: '#C9A96E', desktop: '#5C6B73', tablet: '#8C7B6E' };\nconst deviceLabels = { mobile: 'Mobile', desktop: 'Desktop', tablet: 'Tablet' };\n\nconst deviceRows = devices.map((d, i) => {\n const cat = (d.deviceCategory || 'other').toLowerCase();\n const { wow: wowDev, prevValue } = listWoWWithPrev(d, devicesPrev, 'deviceCategory', 'sessions');\n return barRowWoW(deviceLabels[cat] || cat, d.sessions, totalDeviceSessions, deviceColors[cat] || C.gold, wowDev, prevValue, i % 2 !== 0);\n}).join('');\n\nconst nvrRows = [\n barRowWoW('New Users', newUsersRow.totalUsers, totalNvR, '#2D6A4F', wowNewUsers, parseInt(newUsersPrevRow.totalUsers) || null, false),\n barRowWoW('Returning Users', returningRow.totalUsers, totalNvR, C.gold, wowRetUsers, parseInt(retUsersPrevRow.totalUsers) || null, true),\n].join('');\n\nconst pageRows = pages.map((p, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(p, pagesPrev, 'unifiedScreenName', 'screen_page_view');\n return listRowWoW(i + 1, p.unifiedScreenName, fmt(p.screen_page_view), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst referralRows = referrals.map((r, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(r, referralsPrev, 'sessionSourceMedium', 'sessions');\n return listRowWoW(i + 1, r.sessionSourceMedium, fmt(r.sessions), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst eventRows = events.map((e, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(e, eventsPrev, 'eventName', 'event_count');\n return listRowWoW(i + 1, e.eventName, fmt(e.event_count), w, prevValue, i % 2 !== 0);\n}).join('');\n\nconst countryRows = countries.map((c, i) => {\n const { wow: w, prevValue } = listWoWWithPrev(c, countriesPrev, 'country', 'sessions');\n return listRowWoW(i + 1, c.country, fmt(c.sessions), w, prevValue, i % 2 !== 0);\n}).join('');\n\n// \u2500\u2500 7. COLUMN HEADER DEFINITIONS \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// align MUST match the text-align of the data cell below it\n\nconst hdrsOverview = [{label:'Metric',align:'left'},{label:'This Week',align:'right'},{label:'Last Week',align:'right'},{label:'Change',align:'right'}];\nconst hdrsAudience = [{label:'Segment',align:'left'},{label:'Share',align:'left'},{label:'Users',align:'right'},{label:'Change',align:'right'}];\nconst hdrsScreens = [{label:'#',align:'center'},{label:'Screen / Page',align:'left'},{label:'Views',align:'right'},{label:'Change',align:'right'}];\nconst hdrsTraffic = [{label:'#',align:'center'},{label:'Source / Medium',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\nconst hdrsEvents = [{label:'#',align:'center'},{label:'Event Name',align:'left'},{label:'Count',align:'right'},{label:'Change',align:'right'}];\nconst hdrsGeo = [{label:'#',align:'center'},{label:'Country',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\nconst hdrsDevices = [{label:'Device',align:'left'},{label:'Share',align:'left'},{label:'Sessions',align:'right'},{label:'Change',align:'right'}];\n\n// \u2500\u2500 8. BUILD EMAIL \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\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n <title>Weekly Performance Report</title>\n</head>\n<body style=\"margin:0;padding:0;background:${C.bg};font-family:Arial,sans-serif;\">\n<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:${C.bg};\">\n<tr><td align=\"center\" style=\"padding:32px 16px 40px;\">\n\n <table width=\"600\" cellpadding=\"0\" cellspacing=\"0\"\n style=\"max-width:600px;width:100%;background:${C.card};border-radius:4px;overflow:hidden;box-shadow:0 2px 40px rgba(0,0,0,0.09);\">\n\n <!-- HEADER -->\n <tr>\n <td style=\"background:${C.headerBg};padding:40px 32px 36px;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;font-weight:bold;color:${C.gold};letter-spacing:2.5px;text-transform:uppercase;margin-bottom:14px;\">Analytics Report</div>\n <div style=\"font-family:Georgia,serif;font-size:28px;font-weight:bold;color:#FFFFFF;line-height:1.2;margin-bottom:10px;letter-spacing:-0.5px;\">Weekly Performance<br>Summary</div>\n <div style=\"font-family:Arial,sans-serif;font-size:13px;color:${C.headerSub};margin-bottom:24px;\">${startLabel} \u2014 ${endLabel}</div>\n <div style=\"height:1px;background:linear-gradient(to right,${C.gold},transparent);margin-bottom:24px;\"></div>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n <td style=\"text-align:center;padding:0 12px 0 0;width:25%;border-right:1px solid #2E2E2E;\">\n <div style=\"font-family:Georgia,serif;font-size:22px;font-weight:bold;color:#FFFFFF;\">${fmt(currentWeek.totalUsers)}</div>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-top:4px;text-transform:uppercase;letter-spacing:0.8px;\">Users</div>\n <div style=\"font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wow.totalUsers.color};margin-top:5px;\">${wow.totalUsers.arrow} ${wow.totalUsers.text}</div>\n </td>\n <td style=\"text-align:center;padding:0 12px;width:25%;border-right:1px solid #2E2E2E;\">\n <div style=\"font-family:Georgia,serif;font-size:22px;font-weight:bold;color:#FFFFFF;\">${fmt(currentWeek.sessions)}</div>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-top:4px;text-transform:uppercase;letter-spacing:0.8px;\">Sessions</div>\n <div style=\"font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wow.sessions.color};margin-top:5px;\">${wow.sessions.arrow} ${wow.sessions.text}</div>\n </td>\n <td style=\"text-align:center;padding:0 12px;width:25%;border-right:1px solid #2E2E2E;\">\n <div style=\"font-family:Georgia,serif;font-size:22px;font-weight:bold;color:#FFFFFF;\">${fmtPct(currentWeek.bounce_rate)}</div>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-top:4px;text-transform:uppercase;letter-spacing:0.8px;\">Bounce</div>\n <div style=\"font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wow.bounceRate.color};margin-top:5px;\">${wow.bounceRate.arrow} ${wow.bounceRate.text}</div>\n </td>\n <td style=\"text-align:center;padding:0 0 0 12px;width:25%;\">\n <div style=\"font-family:Georgia,serif;font-size:22px;font-weight:bold;color:#FFFFFF;\">${formatDuration(currentWeek.average_session_dur)}</div>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-top:4px;text-transform:uppercase;letter-spacing:0.8px;\">Duration</div>\n <div style=\"font-family:Arial,sans-serif;font-size:11px;font-weight:bold;color:${wow.avgDuration.color};margin-top:5px;\">${wow.avgDuration.arrow} ${wow.avgDuration.text}</div>\n </td>\n </tr></table>\n </td></tr></table>\n </td>\n </tr>\n\n <!-- BODY -->\n <tr><td style=\"padding:0;background:${C.bg};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n\n ${sectionGap}\n\n <!-- AI SUMMARY -->\n ${formattedAiSummary ? `\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr><td style=\"padding:20px 24px 14px;border-bottom:2px solid ${C.gold};\">\n <span style=\"font-family:Georgia,serif;font-size:15px;font-weight:bold;color:${C.sectionText};letter-spacing:0.3px;\">AI Executive Summary</span>\n <span style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.headerSub};margin-left:10px;text-transform:uppercase;letter-spacing:0.8px;\">Generated by Gemini</span>\n </td></tr>\n <tr><td style=\"padding:20px 24px 24px;background:#FDFCFB;\">${formattedAiSummary}</td></tr>\n </table>\n </td></tr>\n ${sectionGap}` : ''}\n\n <!-- OVERVIEW -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Overview', `vs ${prevStartLabel} \u2013 ${prevEndLabel}`)}\n ${colHeaders(hdrsOverview)}\n ${overviewRow('Total Users', fmt(currentWeek.totalUsers), fmt(previousWeek.totalUsers), wow.totalUsers, false)}\n ${overviewRow('Sessions', fmt(currentWeek.sessions), fmt(previousWeek.sessions), wow.sessions, true)}\n ${overviewRow('New Users', fmt(currentWeek.new_users), fmt(previousWeek.new_users), wow.newUsers, false)}\n ${overviewRow('Bounce Rate', fmtPct(currentWeek.bounce_rate), fmtPct(previousWeek.bounce_rate), wow.bounceRate, true)}\n ${overviewRow('Avg. Session Duration', formatDuration(currentWeek.average_session_dur), formatDuration(previousWeek.average_session_dur), wow.avgDuration, false)}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n <!-- AUDIENCE -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Audience', 'New vs Returning \u00b7 WoW')}\n ${colHeaders(hdrsAudience)}\n ${nvrRows}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n <!-- TOP SCREENS -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Top Screens', 'By views \u00b7 WoW')}\n ${colHeaders(hdrsScreens)}\n ${pageRows}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n <!-- TRAFFIC SOURCES -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Traffic Sources', 'Top channels \u00b7 WoW')}\n ${colHeaders(hdrsTraffic)}\n ${referralRows}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n <!-- TOP EVENTS -->\n ${events.length > 0 ? `\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Top Events', 'User interactions \u00b7 WoW')}\n ${colHeaders(hdrsEvents)}\n ${eventRows}\n </table>\n </td></tr>\n ${sectionGap}` : ''}\n\n <!-- GEOGRAPHY -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Geography', 'Top 5 countries \u00b7 WoW')}\n ${colHeaders(hdrsGeo)}\n ${countryRows}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n <!-- DEVICES -->\n <tr><td colspan=\"5\" style=\"padding:0;background:${C.card};\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n ${sectionHeader('Devices', 'By device category \u00b7 WoW')}\n ${colHeaders(hdrsDevices)}\n ${deviceRows}\n </table>\n </td></tr>\n\n ${sectionGap}\n\n </table>\n </td></tr>\n\n <!-- FOOTER -->\n <tr>\n <td style=\"background:${C.footerBg};padding:32px;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n <tr><td style=\"padding-bottom:16px;\">\n <div style=\"height:1px;background:linear-gradient(to right,${C.gold},transparent);margin-bottom:20px;\"></div>\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr>\n <td>\n <div style=\"font-family:Georgia,serif;font-size:14px;color:#FFFFFF;font-weight:bold;margin-bottom:4px;\">AppStoneLab Technologies</div>\n <a href=\"https://appstonelab.com\" style=\"font-family:Arial,sans-serif;font-size:11px;color:${C.gold};text-decoration:none;\">appstonelab.com</a>\n </td>\n <td style=\"text-align:right;vertical-align:top;\">\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:${C.footerText};line-height:1.8;\">\n ${startLabel} \u2014 ${endLabel}<br>Google Analytics 4<br>\n <a href=\"#\" style=\"color:${C.footerText};text-decoration:underline;\">Unsubscribe</a>\n </div>\n </td>\n </tr></table>\n </td></tr>\n <tr><td>\n <div style=\"font-family:Arial,sans-serif;font-size:10px;color:#3A3A3A;line-height:1.6;\">\n This report is automatically generated every Monday via n8n workflow automation. Data reflects the 7-day period ending ${endLabel}.\n </div>\n </td></tr>\n </table>\n </td>\n </tr>\n\n </table>\n</td></tr>\n</table>\n</body>\n</html>`;\n\n// \u2500\u2500 9. OUTPUT \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\u2500\u2500\u2500\n\nreturn [{\n json: {\n subject: `Weekly Performance Report \u2014 ${startLabel} to ${endLabel}`,\n html,\n recipients: 'user@example.com',\n _debug: {\n currentWeekUsers: currentWeek.totalUsers,\n previousWeekUsers: previousWeek.totalUsers,\n pagesReturned: pages.length,\n eventsReturned: events.length,\n devicesReturned: devices.length,\n aiSummaryLength: aiSummary.length,\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a50596dc-72d1-4efa-8d73-d9f9ed96d5c2",
"name": "Send Weekly Report",
"type": "n8n-nodes-base.emailSend",
"position": [
1248,
112
],
"parameters": {
"html": "={{ $json.html }}",
"options": {},
"subject": "={{ $json.subject }}",
"toEmail": "={{ $json.recipients }}, jignesh.sanghani@dev.appstonelab.com",
"fromEmail": "={{ $json.recipients }}"
},
"credentials": {
"smtp": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "402d7cd9-f814-49f5-9ae9-985d2ae86492",
"name": "GA4 - Top 5 Pages Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
-272
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"name": "screen_page_view",
"listName": "custom",
"expression": "screenPageViews"
},
{
"name": "average_session_duration",
"listName": "custom",
"expression": "averageSessionDuration"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "unifiedScreenName",
"listName": "other"
}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "98a79977-3d6d-4200-ba26-4b2f59868ada",
"name": "GA4 - Top 5 Referrals Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
-80
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "sessionSourceMedium",
"listName": "other"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "cec80406-03d0-4592-9646-e489216e32a7",
"name": "GA4 - Top 5 Events Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
112
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"name": "event_count",
"listName": "custom",
"expression": "eventCount"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "eventName",
"listName": "other"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "=event_count"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "31a96f65-3ff9-47da-b6ea-62e059d636f3",
"name": "GA4 - Top 5 Countries Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
304
],
"parameters": {
"limit": 5,
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"listName": "country"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "7ac51236-fc7e-4e90-9ebc-44c4a897697e",
"name": "GA4 - Device Breakdown Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
496
],
"parameters": {
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"listName": "deviceCategory"
}
]
},
"additionalFields": {
"orderByUI": {
"metricOrderBy": [
{
"desc": true,
"metricName": "sessions"
}
]
}
}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "814f5d15-8fef-4db4-a676-deece444adb0",
"name": "GA4 - New vs Returning Previous Week",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-320,
688
],
"parameters": {
"endDate": "={{$now.minus({days: 8}).startOf('day').toFormat('yyyy-MM-dd')}}",
"dateRange": "custom",
"startDate": "={{$now.minus({days: 14}).startOf('day').toFormat('yyyy-MM-dd')}}",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
},
{}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "481410811"
},
"dimensionsGA4": {
"dimensionValues": [
{
"name": "newVsReturning",
"listName": "other"
}
]
},
"additionalFields": {}
},
"credentials": {
"googleAnalyticsOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "7b51be87-b503-40e8-8516-3965f3ae013d",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
-48
],
"parameters": {
"color": 6,
"width": 544,
"height": 480,
"content": "## \ud83d\udcca Weekly GA4 Business Intelligence Report\n\nAutomatically runs **every Monday at 8:00 AM** and delivers a fully formatted HTML performance report to stakeholders via email.\n\n**What this workflow does:**\n- Fetches 14 separate reports from Google Analytics 4 (Current & Previous week data for 7 key metrics)\n- Generates an AI executive summary analyzing Week-over-Week (WoW) trends using Gemini\n- Calculates WoW % changes for ALL metrics (Overview, Pages, Referrals, Events, Countries, Devices, New vs Returning)\n- Builds a premium HTML email and delivers it via Gmail/SMTP\n\n**Before using this template:**\n1. Set your GA4 Property ID in all 14 GA4 nodes\n2. Connect your Google Analytics OAuth2 credential\n3. Connect your Gemini API credential\n4. Connect your Gmail / SMTP credential\n5. Set your recipient email(s) in the Email node or Code node\n6. Set your timezone in workflow settings"
},
"typeVersion": 1
},
{
"id": "3b842436-77b7-4605-8969-a9b187cbcff2",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1248,
-464
],
"parameters": {
"color": 3,
"width": 688,
"height": 448,
"content": "## \ud83d\udce1 Google Analytics 4 \u2014 Data Nodes\n\nFetches data for 7 key categories, running both **Current Week** and **Previous Week** nodes to generate deep Week-over-Week (WoW) comparisons.\n\n**\u26a0\ufe0f Required: Set your Property ID**\nOpen each of the 14 nodes \u2192 Property ID field \u2192 replace {YOUR_PROPERTY_ID} with your GA4 numeric property ID.\nFound at: GA4 Admin \u2192 Property Settings \u2192 Property ID\n\n**Node breakdown (Each has a Current & Previous week version):**\n- **Overview** \u2014 totals for sessions, users, bounce rate, etc.\n- **Top 5 Pages** \u2014 screens/pages by views\n- **Top 5 Referrals** \u2014 traffic sources by sessions\n- **Top 5 Events** \u2014 user interactions by count\n- **Top 5 Countries** \u2014 geographic breakdown by sessions\n- **Device Breakdown** \u2014 mobile / desktop / tablet split\n- **New vs Returning** \u2014 audience loyalty breakdown\n\n**All nodes have Execute Once = ON** to prevent duplicate rows."
},
"typeVersion": 1
},
{
"id": "7e2f82c1-abb2-45ff-bfdb-d876182d3495",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1328,
48
],
"parameters": {
"color": 7,
"width": 512,
"height": 288,
"content": "## \u23f0 Schedule Trigger\n\nFires every **Monday at 8:00 AM IST**.\n\n\n\n\n\n\n\n\n**To change timezone or time:**\nOpen workflow settings \u2192 change the Timezone to your preferred one.\n\nDefault timezone: Your Local Timezone"
},
"typeVersion": 1
},
{
"id": "d05075f0-2f6d-49ac-978f-fa03e5c90715",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-48,
-272
],
"parameters": {
"color": 4,
"width": 304,
"height": 624,
"content": "## \ud83d\udd00 Merge\n\nWaits for **all GA4 data chains (14 nodes total)** to complete before passing data forward.\n\nWithout this node, Gemini and Code nodes could trigger before all GA4 data is ready - causing empty or undefined values in the report.\n\nMode: Combine by Position"
},
"typeVersion": 1
},
{
"id": "b4fa1e17-e992-49f6-9264-65a8e2569f47",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
272,
-272
],
"parameters": {
"color": 5,
"width": 416,
"height": 624,
"content": "## \ud83e\udd16 Gemini \u2014 AI Executive Summary\n\nGenerates a **3-paragraph executive summary** covering overall performance, audience behaviour, and actionable recommendations. It analyzes the full Week-over-Week dataset from all 14 GA4 nodes to spot trends.\n\n**Credential required:** Google Gemini API\nGet your key at: aistudio.google.com\n\n**Model:** gemini-3.1-flash-lite-preview\n\nIf Gemini fails or returns empty, the report still sends normally \u2014 the AI section is hidden from the email automatically."
},
"typeVersion": 1
},
{
"id": "130b47cc-f010-46a4-b926-340d8237ec7f",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
-272
],
"parameters": {
"color": 4,
"width": 432,
"height": 624,
"content": "## \ud83e\uddee Code Node \u2014 Data Processing + Email Builder\n\nDoes 4 things:\n1. Pulls data from all 14 GA4 nodes and Gemini by node name.\n2. Calculates week-over-week % changes for **every single section** (Overview, Pages, Sources, Events, Countries, Devices, New vs Returning).\n3. Builds the full HTML email with inline CSS and alignment fixes.\n4. Outputs subject, html, and recipients for the Email node.\n\n**To change recipients:**\nFind `recipients:` near the bottom of the code \u2192 update the email address.\nMultiple: `'email1@gmail.com,email2@gmail.com'`\n\n\n\n\n\n\n\n\n\n\n\n\n**To change client name in footer:**\nFind `AppStoneLab Technologies` in the HTML and replace with your client's brand name."
},
"typeVersion": 1
},
{
"id": "8c2b8818-4b6f-460b-9896-5d13240bd3fb",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1152,
-272
],
"parameters": {
"color": 2,
"width": 288,
"height": 624,
"content": "## \ud83d\udce7 Send Email\n\nSends the HTML report to all recipients defined in the Code node.\n\n**Fields mapped from Code node output:**\n- To \u2192 {{ $json.recipients }}\n- Subject \u2192 {{ $json.subject }}\n- HTML Body \u2192 {{ $json.html }}\n\n**Credential required:** Gmail OAuth2 or SMTP\nFor SMTP: update host, port, and login credentials in the credential settings."
},
"typeVersion": 1
}
],
"active": false,
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.
googleAnalyticsOAuth2googlePalmApismtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Stop manually building weekly analytics reports. This workflow automatically fetches your GA4 data every Monday morning, generates an AI-written executive summary using Gemini, builds a premium formatted HTML email with deep Week-over-Week (WoW) comparisons for every metric, and…
Source: https://n8n.io/workflows/13993/ — 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 workflow runs on scheduled weekly and monthly triggers to generate unified marketing performance reports. It processes multiple websites by collecting analytics data, paid ads performance, and CR
Fetch Multiple Google Analytics GA4 metrics daily, post to Discord, update previous day’s entry as GA data finalizes over seven days. Automates daily traffic reporting Maintains single message per day
This workflow automates the daily backfill of Google Analytics 4 (GA4) data into BigQuery. It fetches 13 essential pre-processed reports (including User Acquisition, Traffic, and E-commerce) and uploa
Google analytics template. Uses scheduleTrigger, manualTrigger, stickyNote, googleAnalytics. Scheduled trigger; 22 nodes.
If you own a website and need to analyze your Google analytics data If you need to create an SEO report on which pages are getting most traffic or how your google search terms are performing If you wa