This workflow corresponds to n8n.io template #16218 — we link there as the canonical source.
This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "a437aa2e-624d-43df-b160-0848edf88715",
"name": "Sticky Note \u2014 Airtable Setup",
"type": "n8n-nodes-base.stickyNote",
"position": [
144,
624
],
"parameters": {
"color": 7,
"width": 548,
"height": 688,
"content": "## \ud83d\uddc4\ufe0f Airtable Setup\n\n**Table 1: `Clients`**\n\n| Field | Type |\n|---|---|\n| `active` | Single line text (`yes` / `no`) |\n| `site_name` | Single line text |\n| `umami_site_id` | Single line text |\n| `email` | Email |\n| `slack_webhook` | URL |\n| `language` | Single line text (`en`, `bs-latin`, `hr`, `sr-latin`, `sr-cyrillic`, `mk`) |\n| `history_summary` | Long text (auto-updated by workflow) |\n\n**Table 2: `History`**\n\n| Field | Type |\n|---|---|\n| `client_id` | Single line text |\n| `site_name` | Single line text |\n| `period` | Single line text |\n| `visitors` | Number |\n| `visitors_wow` | Number |\n| `organic_visitors` | Number |\n| `bounce_rate` | Number |\n| `summary` | Long text |\n| `report_text` | Long text |\n\n**Get your Airtable API key:**\nairtable.com/create/tokens\nScopes needed: `data.records:read` + `data.records:write`\n\n**Get your Base ID:**\nFrom your base URL: `airtable.com/appXXXXXXXXX/...`"
},
"typeVersion": 1
},
{
"id": "764afbc2-aaaf-4f8f-a7e7-aec14ce38a70",
"name": "Sticky Note \u2014 Config",
"type": "n8n-nodes-base.stickyNote",
"position": [
976,
176
],
"parameters": {
"color": 7,
"width": 264,
"height": 320,
"content": "## Step 1 \u2014 Config\nFill in all values here.\nNo credentials are stored anywhere else in the workflow."
},
"typeVersion": 1
},
{
"id": "a5faceb3-2b7f-4632-aa75-894a61d08990",
"name": "Sticky Note \u2014 Auth",
"type": "n8n-nodes-base.stickyNote",
"position": [
1264,
176
],
"parameters": {
"color": 7,
"width": 352,
"height": 324,
"content": "## Step 2 \u2014 Umami Auth\nLogs in to Umami and stores\nthe bearer token for all\nsubsequent API calls."
},
"typeVersion": 1
},
{
"id": "2b6c50a7-16f8-40d3-9ab7-77a9b31b72ba",
"name": "Sticky Note \u2014 Loop",
"type": "n8n-nodes-base.stickyNote",
"position": [
1632,
112
],
"parameters": {
"color": 7,
"width": 888,
"height": 392,
"content": "## Step 3 \u2014 Client Loop\nFetches active clients from Airtable\nand loops through one at a time.\n\nThe `Has client?` IF node prevents\nan empty second pass after the\nlast client is processed."
},
"typeVersion": 1
},
{
"id": "cdc8c7e8-ca74-4d0a-baf1-2d02b48d3649",
"name": "Sticky Note \u2014 Umami Data",
"type": "n8n-nodes-base.stickyNote",
"position": [
2544,
-352
],
"parameters": {
"color": 7,
"width": 460,
"height": 956,
"content": "## Step 4 \u2014 Umami Data\nFetches 4 data points in parallel:\n- This week stats (visitors, pageviews, bounce, duration)\n- Prior week stats (for week-over-week comparison)\n- Top pages by URL path\n- Traffic sources by referrer\n\nAll 4 results are merged before processing."
},
"typeVersion": 1
},
{
"id": "e2ccdec7-18bf-4fd9-ad3b-d0db2a140424",
"name": "Sticky Note \u2014 AI Report",
"type": "n8n-nodes-base.stickyNote",
"position": [
3024,
-208
],
"parameters": {
"color": 7,
"width": 616,
"height": 536,
"content": "## Step 5 \u2014 AI Report\nGroq (llama-3.3-70b) generates a structured\nSEO + AIO report in the client's language.\n\nReport sections:\n1. WEEKLY SUMMARY\n2. TOP 3 WINS\n3. TOP 3 ISSUES\n4. SEO RECOMMENDATIONS\n5. AIO RECOMMENDATIONS\n6. QUICK WIN\n\nLanguage is set per-client in Airtable.\nSupports: `en`, `bs-latin`, `sr-latin`,\n`sr-cyrillic`, `hr`, `mk`"
},
"typeVersion": 1
},
{
"id": "3ecc51ae-c67e-4f42-96fc-5b80ef444db0",
"name": "Sticky Note \u2014 Delivery",
"type": "n8n-nodes-base.stickyNote",
"position": [
3664,
-80
],
"parameters": {
"color": 7,
"width": 648,
"height": 412,
"content": "## Step 6 \u2014 Delivery\nReport sent via 3 channels sequentially:\n1. **Email** \u2014 branded HTML report\n2. **Slack** \u2014 Block Kit summary card\n3. **Airtable** \u2014 saved to History table\n\n\u26a0\ufe0f Add your SMTP credential\nto the `Send email report` node."
},
"typeVersion": 1
},
{
"id": "67f8f907-5c26-4df0-8580-35279e3981ba",
"name": "Every Monday 07:00",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
832,
328
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1
],
"triggerAtHour": 7
}
]
}
},
"typeVersion": 1.1
},
{
"id": "cb2e930f-e4f1-4431-a004-1c16c247d012",
"name": "Config",
"type": "n8n-nodes-base.set",
"position": [
1056,
328
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "1",
"name": "GROQ_MODEL",
"type": "string",
"value": "llama-3.3-70b-versatile"
},
{
"id": "2",
"name": "GROQ_MAX_TOKENS",
"type": "number",
"value": 1500
},
{
"id": "3",
"name": "GROQ_TEMPERATURE",
"type": "number",
"value": 0.4
},
{
"id": "4",
"name": "DEFAULT_LANGUAGE",
"type": "string",
"value": "en"
},
{
"id": "5",
"name": "RATE_LIMIT_SECS",
"type": "number",
"value": 2
},
{
"id": "6",
"name": "UMAMI_URL",
"type": "string",
"value": "https://YOUR_UMAMI_DOMAIN"
},
{
"id": "7",
"name": "UMAMI_USER",
"type": "string",
"value": "YOUR_UMAMI_USERNAME"
},
{
"id": "8",
"name": "UMAMI_PASS",
"type": "string",
"value": "YOUR_UMAMI_PASSWORD"
},
{
"id": "9",
"name": "AIRTABLE_API_KEY",
"type": "string",
"value": "YOUR_AIRTABLE_API_KEY"
},
{
"id": "10",
"name": "AIRTABLE_BASE_ID",
"type": "string",
"value": "YOUR_AIRTABLE_BASE_ID"
},
{
"id": "11",
"name": "GROQ_API_KEY",
"type": "string",
"value": "YOUR_GROQ_API_KEY"
},
{
"id": "12",
"name": "MAILER_FROM",
"type": "string",
"value": "YOUR_SENDING_EMAIL"
},
{
"id": "13",
"name": "ERROR_SLACK_WEBHOOK",
"type": "string",
"value": "YOUR_SLACK_WEBHOOK_URL"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b6c0ada4-52a0-4ef4-9100-c1a6e917cc0f",
"name": "Umami \u2014 login",
"type": "n8n-nodes-base.httpRequest",
"position": [
1280,
328
],
"parameters": {
"url": "={{ $('Config').first().json.UMAMI_URL }}/api/auth/login",
"body": "={{ JSON.stringify({ username: $('Config').first().json.UMAMI_USER, password: $('Config').first().json.UMAMI_PASS }) }}",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"specifyBody": "string",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "644c47cf-ca4f-47cd-b5c3-778a896afd17",
"name": "Store token",
"type": "n8n-nodes-base.set",
"position": [
1504,
328
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "1",
"name": "_umami_token",
"type": "string",
"value": "={{ $json.token }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "ef3c737f-fa9f-4b53-b6f2-750d006d451c",
"name": "Get client list",
"type": "n8n-nodes-base.httpRequest",
"position": [
1728,
328
],
"parameters": {
"url": "=https://api.airtable.com/v0/{{ $('Config').first().json.AIRTABLE_BASE_ID }}/Clients?filterByFormula={active}='yes'",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Config').first().json.AIRTABLE_API_KEY }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "5efaf02b-e931-4580-993b-9669c652a3db",
"name": "Flatten records + inject token",
"type": "n8n-nodes-base.code",
"position": [
1952,
328
],
"parameters": {
"jsCode": "const records = $input.first().json.records || [];\nconst token = $('Store token').first().json._umami_token;\n\nconst now = Date.now();\nconst DAY = 24 * 60 * 60 * 1000;\n\nreturn records.map(r => ({\n json: {\n ...r.fields,\n _airtable_id: r.id,\n _umami_token: token,\n _thisStart: now - 7 * DAY,\n _thisEnd: now,\n _priorStart: now - 14 * DAY,\n _priorEnd: now - 7 * DAY\n }\n}));"
},
"typeVersion": 2
},
{
"id": "47a09b17-3cc5-44fe-b118-be79c809c066",
"name": "Loop each client",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2176,
328
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "8eaf1c9f-7cf4-4754-b541-44aafd82f8f3",
"name": "Has client?",
"type": "n8n-nodes-base.if",
"position": [
2400,
144
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.umami_site_id }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "bd30d650-2973-4186-984e-07c24abaaa6f",
"name": "Umami \u2014 this week",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
-128
],
"parameters": {
"url": "={{ $('Config').first().json.UMAMI_URL }}/api/websites/{{ $('Loop each client').first().json.umami_site_id }}/stats",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "startAt",
"value": "={{ $('Loop each client').first().json._thisStart }}"
},
{
"name": "endAt",
"value": "={{ $('Loop each client').first().json._thisEnd }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Store token').first().json._umami_token }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "e616f2d7-766c-4a2f-b411-080cd3df11ca",
"name": "Umami \u2014 prior week",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
64
],
"parameters": {
"url": "={{ $('Config').first().json.UMAMI_URL }}/api/websites/{{ $('Loop each client').first().json.umami_site_id }}/stats",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "startAt",
"value": "={{ $('Loop each client').first().json._priorStart }}"
},
{
"name": "endAt",
"value": "={{ $('Loop each client').first().json._priorEnd }}"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Store token').first().json._umami_token }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "c82c48b3-8e21-4872-a575-a063dab6da97",
"name": "Umami \u2014 top pages",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
256
],
"parameters": {
"url": "={{ $('Config').first().json.UMAMI_URL }}/api/websites/{{ $('Loop each client').first().json.umami_site_id }}/metrics",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "startAt",
"value": "={{ $('Loop each client').first().json._thisStart }}"
},
{
"name": "endAt",
"value": "={{ $('Loop each client').first().json._thisEnd }}"
},
{
"name": "type",
"value": "path"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Store token').first().json._umami_token }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "0d25d487-32c7-4c4b-b40d-621e2b8bf688",
"name": "Umami \u2014 traffic sources",
"type": "n8n-nodes-base.httpRequest",
"position": [
2624,
448
],
"parameters": {
"url": "={{ $('Config').first().json.UMAMI_URL }}/api/websites/{{ $('Loop each client').first().json.umami_site_id }}/metrics",
"options": {},
"sendQuery": true,
"sendHeaders": true,
"queryParameters": {
"parameters": [
{
"name": "startAt",
"value": "={{ $('Loop each client').first().json._thisStart }}"
},
{
"name": "endAt",
"value": "={{ $('Loop each client').first().json._thisEnd }}"
},
{
"name": "type",
"value": "referrer"
}
]
},
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Store token').first().json._umami_token }}"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "c8b98efa-86c5-4d64-ada7-744088ab228d",
"name": "Merge \u2014 all Umami data",
"type": "n8n-nodes-base.merge",
"position": [
2848,
128
],
"parameters": {
"numberInputs": 4
},
"typeVersion": 3
},
{
"id": "b5307c54-23a4-4b26-8695-31dba0d9a5a8",
"name": "Format data + build AI prompt",
"type": "n8n-nodes-base.code",
"position": [
3072,
160
],
"parameters": {
"jsCode": "const client = $('Loop each client').first().json;\nconst cfg = $('Config').first().json;\n\nconst allItems = $input.all().map(i => i.json);\n\nconst statsItems = allItems.filter(i => i.visitors !== undefined && i.x === undefined && !Array.isArray(i));\nconst metricItems = allItems.filter(i => i.x !== undefined && i.y !== undefined);\n\nconst cw = statsItems[0] || {};\nconst pw = statsItems[1] || {};\n\nconst topPagesRaw = metricItems.filter(i => String(i.x).startsWith('/'));\nconst sourcesRaw = metricItems.filter(i => !String(i.x).startsWith('/'));\n\nfunction parseStats(raw) {\n const v = (field) => (field && typeof field === 'object') ? (field.value || 0) : (field || 0);\n const visits = v(raw.visits);\n const bounces = v(raw.bounces);\n const totaltime = v(raw.totaltime);\n return {\n visitors: v(raw.visitors),\n pageviews: v(raw.pageviews),\n visits,\n bounce_rate: visits > 0 ? Math.round((bounces / visits) * 100) : 0,\n visit_duration: visits > 0 ? Math.round(totaltime / visits) : 0\n };\n}\n\nconst curr = parseStats(cw);\nconst prior = parseStats(pw);\n\nconst topPages = topPagesRaw.map(p => ({ page: p.x, visitors: p.y }));\nconst sources = sourcesRaw.map(s => ({ source: s.x || 'Direct', visitors: s.y }));\n\nfunction wow(c, p) {\n if (!p || p === 0) return { value: c, prior: 0, pct: 0, arrow: '\u2013' };\n const pct = parseFloat(((c - p) / p * 100).toFixed(1));\n return { value: c, prior: p, pct, arrow: pct >= 0 ? '\u25b2' : '\u25bc' };\n}\n\nconst vWow = wow(curr.visitors, prior.visitors);\nconst pWow = wow(curr.pageviews, prior.pageviews);\nconst bWow = wow(curr.bounce_rate, prior.bounce_rate);\nconst dWow = wow(curr.visit_duration, prior.visit_duration);\n\nconst googleSrc = sources.find(s => /google/i.test(s.source));\nconst organicVisitors = googleSrc ? googleSrc.visitors : 0;\nconst organicPct = curr.visitors > 0 ? ((organicVisitors / curr.visitors) * 100).toFixed(1) : '0.0';\n\nconst pagesTable = topPages.slice(0, 15).map(p => ` ${String(p.page).padEnd(40)} ${String(p.visitors).padStart(6)} visitors`).join('\\n');\nconst sourcesTable = sources.map(s => ` ${String(s.source).padEnd(25)} ${s.visitors} visitors`).join('\\n');\n\nconst now = new Date();\nconst weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);\nconst fmt = d => d.toISOString().split('T')[0];\nconst period = `${fmt(weekAgo)} to ${fmt(now)}`;\n\nconst historyCtx = client.history_summary ? `\\nHISTORY (prior 3 weeks):\\n${client.history_summary}\\n` : '';\n\nconst langMap = {\n 'en': 'Write in English.',\n 'bs-latin': 'Write in Bosnian (Latin script). Keep technical SEO terms in English.',\n 'sr-latin': 'Write in Serbian (Latin script). Keep technical SEO terms in English.',\n 'sr-cyrillic': 'Write in Serbian (Cyrillic). Keep technical SEO terms in English.',\n 'hr': 'Write in Croatian. Keep technical SEO terms in English.',\n 'mk': 'Write in Macedonian. Keep technical SEO terms in English.'\n};\nconst langInstruction = langMap[client.language || cfg.DEFAULT_LANGUAGE] || langMap['en'];\n\nconst systemPrompt = `You are a senior SEO and AIO (AI-search optimisation) analyst producing weekly reports for SMBs.\n\nRules:\n- Ground every recommendation in the provided data. Never invent metrics.\n- Prioritise by impact-vs-effort (quick wins first).\n- Be specific: name the page, the metric. No generic advice.\n- AIO = optimising for Google AI Overviews, ChatGPT, Perplexity. Flag content at risk.\n- ${langInstruction}\n\nOutput exactly these sections in order, using these exact labels:\n1. WEEKLY SUMMARY \u2014 3 sentences, headline numbers\n2. TOP 3 WINS \u2014 what worked and why\n3. TOP 3 ISSUES \u2014 what needs attention with specific data\n4. SEO RECOMMENDATIONS \u2014 3-5 prioritised actions, each tagged LOW/MED/HIGH effort\n5. AIO RECOMMENDATIONS \u2014 2-3 AI-search visibility actions\n6. QUICK WIN \u2014 one specific action completable in under 2 hours`;\n\nconst userMessage = `SITE: ${client.site_name} (${client.umami_site_id})\nPERIOD: ${period}\n\nHEADLINE METRICS:\n Visitors: ${vWow.value} (${vWow.arrow}${Math.abs(vWow.pct)}% vs prior week, was ${vWow.prior})\n Pageviews: ${pWow.value} (${pWow.arrow}${Math.abs(pWow.pct)}%)\n Bounce rate: ${curr.bounce_rate}% (${bWow.arrow}${Math.abs(bWow.pct)}%)\n Avg session: ${curr.visit_duration}s (${dWow.arrow}${Math.abs(dWow.pct)}%)\n\nORGANIC (Google): ${organicVisitors} visitors (${organicPct}% of total)\n\nTRAFFIC SOURCES:\n${sourcesTable || ' No data'}\n\nTOP PAGES:\n${pagesTable || ' No data'}${historyCtx}`;\n\nreturn [{ json: {\n client,\n systemPrompt,\n userMessage,\n cfg,\n meta: {\n visitors: vWow.value,\n visitors_wow: vWow.pct,\n pageviews: pWow.value,\n bounce_rate: curr.bounce_rate,\n organic_visitors: organicVisitors,\n period\n }\n}}];"
},
"typeVersion": 2
},
{
"id": "3ce72c64-47fa-4a8e-8007-5146929f90a2",
"name": "Groq \u2014 generate report",
"type": "n8n-nodes-base.httpRequest",
"position": [
3296,
160
],
"parameters": {
"url": "https://api.groq.com/openai/v1/chat/completions",
"body": "={{ JSON.stringify({ model: $('Format data + build AI prompt').item.json.cfg.GROQ_MODEL, max_tokens: $('Format data + build AI prompt').item.json.cfg.GROQ_MAX_TOKENS, temperature: $('Format data + build AI prompt').item.json.cfg.GROQ_TEMPERATURE, messages: [{ role: 'system', content: $('Format data + build AI prompt').item.json.systemPrompt }, { role: 'user', content: $('Format data + build AI prompt').item.json.userMessage }] }) }}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"rawContentType": "application/json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Format data + build AI prompt').item.json.cfg.GROQ_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "348b3d27-a365-4732-8a7f-4432d24fe521",
"name": "Parse + format report",
"type": "n8n-nodes-base.code",
"position": [
3520,
160
],
"parameters": {
"jsCode": "const client = $('Format data + build AI prompt').item.json.client;\nconst meta = $('Format data + build AI prompt').item.json.meta;\nconst cfg = $('Format data + build AI prompt').item.json.cfg;\nconst aiText = $json.choices[0].message.content;\n\nconst quickWinMatch = aiText.match(/QUICK WIN[:\\s]*([\\s\\S]*?)(?=\\n[A-Z]{2,}|$)/i);\nconst quickWin = quickWinMatch ? quickWinMatch[1].trim() : 'See full report for this week\\'s quick win.';\n\nconst wowSign = meta.visitors_wow >= 0 ? '+' : '';\nconst wowEmoji = meta.visitors_wow >= 0 ? '\ud83d\udcc8' : '\ud83d\udcc9';\n\nconst htmlBody = aiText.split('\\n').map(line => {\n const t = line.trim();\n if (/^(WEEKLY SUMMARY|TOP 3 WINS|TOP 3 ISSUES|SEO RECOMMENDATIONS|AIO RECOMMENDATIONS|QUICK WIN)/i.test(t))\n return `<h3 style=\"color:#1a1a2e;margin:24px 0 8px;font-size:15px;font-weight:600;border-bottom:1px solid #e5e7eb;padding-bottom:6px\">${t}</h3>`;\n if (t.startsWith('-') || /^\\d+\\./.test(t))\n return `<li style=\"margin:4px 0;color:#374151;line-height:1.6\">${t.replace(/^[-\\d.]+\\s*/, '')}</li>`;\n if (t === '') return '<br>';\n return `<p style=\"margin:4px 0;color:#374151;line-height:1.6\">${t}</p>`;\n}).join('\\n');\n\nconst slackBlocks = {\n blocks: [\n { type: 'header', text: { type: 'plain_text', text: `${wowEmoji} Weekly SEO Report \u2014 ${client.site_name}`, emoji: true } },\n {\n type: 'section',\n fields: [\n { type: 'mrkdwn', text: `*Visitors*\\n${meta.visitors} (${wowSign}${meta.visitors_wow}%)` },\n { type: 'mrkdwn', text: `*Organic*\\n${meta.organic_visitors}` },\n { type: 'mrkdwn', text: `*Bounce Rate*\\n${meta.bounce_rate}%` },\n { type: 'mrkdwn', text: `*Period*\\n${meta.period}` }\n ]\n },\n { type: 'divider' },\n { type: 'section', text: { type: 'mrkdwn', text: `\u26a1 *This week's quick win*\\n${quickWin}` } },\n { type: 'context', elements: [{ type: 'mrkdwn', text: 'Full report sent to your email.' }] }\n ]\n};\n\nconst historySummary = `Week ${meta.period}: ${meta.visitors} visitors (${wowSign}${meta.visitors_wow}% WoW), ${meta.organic_visitors} organic, bounce rate ${meta.bounce_rate}%.`;\n\nreturn [{ json: { client, meta, cfg, aiText, htmlBody, quickWin, slackBlocks, historySummary } }];"
},
"typeVersion": 2
},
{
"id": "36d4aef2-3cdf-4c90-a82a-89b678f2045d",
"name": "Send email report",
"type": "n8n-nodes-base.emailSend",
"position": [
3744,
160
],
"parameters": {
"html": "=<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width'></head><body style='font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:#f9fafb;margin:0;padding:0'><div style='max-width:640px;margin:32px auto;background:#fff;border-radius:12px;border:1px solid #e5e7eb;overflow:hidden'><div style='background:#1a1a2e;padding:24px 32px'><h1 style='color:#fff;margin:0;font-size:20px;font-weight:600'>Weekly SEO Report</h1><p style='color:#9ca3af;margin:4px 0 0;font-size:14px'>{{ $json.client.site_name }} · {{ $json.meta.period }}</p></div><div style='padding:16px 32px;background:#f0f4ff;display:flex;gap:16px;border-bottom:1px solid #e5e7eb'><div style='flex:1;text-align:center'><p style='margin:0;font-size:12px;color:#6b7280'>Visitors</p><p style='margin:4px 0 0;font-size:24px;font-weight:600;color:#1a1a2e'>{{ $json.meta.visitors }}</p><p style='margin:0;font-size:12px;color:{{ $json.meta.visitors_wow >= 0 ? \"#059669\" : \"#dc2626\" }}'>{{ $json.meta.visitors_wow >= 0 ? \"\u25b2\" : \"\u25bc\" }}{{ Math.abs($json.meta.visitors_wow) }}%</p></div><div style='flex:1;text-align:center'><p style='margin:0;font-size:12px;color:#6b7280'>Organic</p><p style='margin:4px 0 0;font-size:24px;font-weight:600;color:#1a1a2e'>{{ $json.meta.organic_visitors }}</p></div><div style='flex:1;text-align:center'><p style='margin:0;font-size:12px;color:#6b7280'>Bounce Rate</p><p style='margin:4px 0 0;font-size:24px;font-weight:600;color:#1a1a2e'>{{ $json.meta.bounce_rate }}%</p></div></div><div style='padding:24px 32px'>{{ $json.htmlBody }}<div style='margin:24px 0;padding:16px;background:#fef3c7;border-radius:8px;border-left:4px solid #f59e0b'><p style='margin:0;font-size:13px;font-weight:600;color:#92400e'>\u26a1 This week's quick win</p><p style='margin:8px 0 0;font-size:14px;color:#78350f'>{{ $json.quickWin }}</p></div></div><div style='padding:16px 32px;border-top:1px solid #e5e7eb;background:#f9fafb'><p style='margin:0;font-size:12px;color:#9ca3af'>View live analytics at <a href='{{ $json.cfg.UMAMI_URL }}/websites/{{ $json.client.umami_site_id }}' style='color:#6366f1'>your Umami dashboard</a>.</p></div></div></body></html>",
"options": {},
"subject": "=Weekly SEO Report \u2014 {{ $json.client.site_name }} \u2014 {{ $json.meta.period }}",
"toEmail": "={{ $json.client.email }}",
"fromEmail": "={{ $json.cfg.MAILER_FROM }}"
},
"typeVersion": 2.1
},
{
"id": "c65f2d77-5df0-4e30-a695-846d375ffd97",
"name": "Send Slack message",
"type": "n8n-nodes-base.httpRequest",
"position": [
3968,
160
],
"parameters": {
"url": "={{ $('Parse + format report').first().json.client.slack_webhook }}",
"body": "={{ JSON.stringify($('Parse + format report').first().json.slackBlocks) }}",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"specifyBody": "string",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "e1a71a3c-b5b7-4111-bf3d-8024a221a4ae",
"name": "Save history to Airtable",
"type": "n8n-nodes-base.httpRequest",
"position": [
4192,
160
],
"parameters": {
"url": "=https://api.airtable.com/v0/{{ $('Parse + format report').first().json.cfg.AIRTABLE_BASE_ID }}/History",
"body": "={{ JSON.stringify({ records: [{ fields: { client_id: $('Parse + format report').first().json.client._airtable_id, site_name: $('Parse + format report').first().json.client.site_name, period: $('Parse + format report').first().json.meta.period, visitors: $('Parse + format report').first().json.meta.visitors, visitors_wow: $('Parse + format report').first().json.meta.visitors_wow, organic_visitors: $('Parse + format report').first().json.meta.organic_visitors, bounce_rate: $('Parse + format report').first().json.meta.bounce_rate, summary: $('Parse + format report').first().json.historySummary, report_text: $('Parse + format report').first().json.aiText } }] }) }}",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"specifyBody": "string",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $('Parse + format report').first().json.cfg.AIRTABLE_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "469e0efc-8ca9-4852-81e0-630416e04ffc",
"name": "Wait (rate limit)",
"type": "n8n-nodes-base.wait",
"position": [
4416,
328
],
"parameters": {
"amount": "={{ $('Config').first().json.RATE_LIMIT_SECS }}"
},
"typeVersion": 1.1
},
{
"id": "97b527ab-3265-45cd-87b0-a67df5047a40",
"name": "Log error to Slack",
"type": "n8n-nodes-base.httpRequest",
"position": [
832,
608
],
"parameters": {
"url": "={{ $('Config').first().json.ERROR_SLACK_WEBHOOK }}",
"body": "={{ JSON.stringify({ text: '\u274c SEO report workflow error: ' + $json.message }) }}",
"method": "POST",
"options": {},
"sendBody": true,
"sendHeaders": true,
"specifyBody": "string",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.1
},
{
"id": "e0ac6dd2-8dc5-4c70-b580-c119e07c45eb",
"name": "Sticky Note \u2014 Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
144,
-160
],
"parameters": {
"width": 540,
"height": 760,
"content": "# \ud83d\udcca Weekly SEO Report \u2014 Umami + Groq + Airtable\n\n**Who it's for:**\nFreelance SEO consultants and digital agencies managing multiple client websites who want automated, AI-written weekly reporting without a paid analytics platform.\n\n**What it does:**\nEvery Monday at 07:00, this workflow fetches the past 7 days of analytics from your self-hosted Umami instance for each active client in Airtable. It compares visitors, pageviews, bounce rate, and session duration against the prior week, identifies top pages and traffic sources, then sends the data to Groq (Llama 3.3 70B) to generate a structured SEO + AIO report. Delivered via branded HTML email, Slack Block Kit summary, and saved to Airtable history.\n\n---\n\n## \u2699\ufe0f Setup \u2014 5 Steps\n\n**1.** Fill in the **Config** node with all your credentials and settings\n\n**2.** Add your **SMTP credential** to the `Send email report` node\n\n**3.** Create two Airtable tables \u2014 see the **Airtable Setup** sticky note below\n\n**4.** Add clients to the `Clients` table with `active = yes`\n\n**5.** Run manually to test, then activate the schedule\n\n---\n\n## \ud83d\udccb Requirements\n- Self-hosted Umami (v2+)\n- Airtable account (free tier works)\n- Groq API key \u2014 free at console.groq.com\n- SMTP email account\n- Self-hosted n8n (v2.3+)"
},
"typeVersion": 1
}
],
"connections": {
"Config": {
"main": [
[
{
"node": "Umami \u2014 login",
"type": "main",
"index": 0
}
]
]
},
"Has client?": {
"main": [
[
{
"node": "Umami \u2014 this week",
"type": "main",
"index": 0
},
{
"node": "Umami \u2014 prior week",
"type": "main",
"index": 0
},
{
"node": "Umami \u2014 top pages",
"type": "main",
"index": 0
},
{
"node": "Umami \u2014 traffic sources",
"type": "main",
"index": 0
}
]
]
},
"Store token": {
"main": [
[
{
"node": "Get client list",
"type": "main",
"index": 0
}
]
]
},
"Get client list": {
"main": [
[
{
"node": "Flatten records + inject token",
"type": "main",
"index": 0
}
]
]
},
"Umami \u2014 login": {
"main": [
[
{
"node": "Store token",
"type": "main",
"index": 0
}
]
]
},
"Loop each client": {
"main": [
[],
[
{
"node": "Has client?",
"type": "main",
"index": 0
}
]
]
},
"Send email report": {
"main": [
[
{
"node": "Send Slack message",
"type": "main",
"index": 0
}
]
]
},
"Wait (rate limit)": {
"main": [
[
{
"node": "Loop each client",
"type": "main",
"index": 0
}
]
]
},
"Every Monday 07:00": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Send Slack message": {
"main": [
[
{
"node": "Save history to Airtable",
"type": "main",
"index": 0
}
]
]
},
"Umami \u2014 this week": {
"main": [
[
{
"node": "Merge \u2014 all Umami data",
"type": "main",
"index": 0
}
]
]
},
"Umami \u2014 top pages": {
"main": [
[
{
"node": "Merge \u2014 all Umami data",
"type": "main",
"index": 2
}
]
]
},
"Umami \u2014 prior week": {
"main": [
[
{
"node": "Merge \u2014 all Umami data",
"type": "main",
"index": 1
}
]
]
},
"Parse + format report": {
"main": [
[
{
"node": "Send email report",
"type": "main",
"index": 0
}
]
]
},
"Groq \u2014 generate report": {
"main": [
[
{
"node": "Parse + format report",
"type": "main",
"index": 0
}
]
]
},
"Merge \u2014 all Umami data": {
"main": [
[
{
"node": "Format data + build AI prompt",
"type": "main",
"index": 0
}
]
]
},
"Save history to Airtable": {
"main": [
[
{
"node": "Wait (rate limit)",
"type": "main",
"index": 0
}
]
]
},
"Umami \u2014 traffic sources": {
"main": [
[
{
"node": "Merge \u2014 all Umami data",
"type": "main",
"index": 3
}
]
]
},
"Format data + build AI prompt": {
"main": [
[
{
"node": "Groq \u2014 generate report",
"type": "main",
"index": 0
}
]
]
},
"Flatten records + inject token": {
"main": [
[
{
"node": "Loop each client",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs every Monday and generates per-client weekly SEO and AIO reports by pulling analytics from Umami, using Groq to write the report, emailing a branded HTML version, posting a Slack summary via webhook, and storing the results in Airtable. Runs every Monday at…
Source: https://n8n.io/workflows/16218/ — 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.
Security teams, DevOps engineers, vulnerability analysts, and automation builders who want to eliminate repetitive Nessus scan parsing, AI-based risk triage, and manual reporting. Designed for orgs fo
This n8n workflow automatically finds apartments for rent in Germany, filters them by your city, rent budget, and number of rooms, and applies to them via email. Each application includes: A personali
This workflow runs weekly to query multiple LLMs via OpenRouter with prompts from Google Sheets, analyzes brand mentions and domain citations, aggregates weekly Share of Voice metrics, and emails an H
👤 Who it’s for Blue Team leads, CISOs, and SOC managers who want automated visibility into threat metrics, endpoint alerts, and response actions — without needing a full SIEM or BI platform.
Workflow Overview Zoom Attendance Evaluator with Follow-up is an n8n automation workflow that automatically evaluates Zoom meeting attendance and sends follow-up emails to no-shows and early leavers w