This workflow corresponds to n8n.io template #17796 — we link there as the canonical source.
This workflow follows the Gmail → 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 →
{
"id": "iofOPraba3ciLQ3f",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Daily Core Web Vitals Monitor (Mobile & Desktop) \u2192 Gmail",
"tags": [],
"nodes": [
{
"id": "12a77d57-8f1f-4adc-a8d4-47d9b2612656",
"name": "Every Day 7 AM",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
0,
96
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 7
}
]
}
},
"typeVersion": 1.3
},
{
"id": "4b8eba72-9bca-474b-a3c6-bf0c4f141ea3",
"name": "Config",
"type": "n8n-nodes-base.set",
"notes": "Edit the two values here: the website URL to audit and the email address that receives the report.",
"position": [
224,
96
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "url-1",
"name": "url",
"type": "string",
"value": "https://pocketfmmodapk.com/"
},
{
"id": "email-1",
"name": "recipientEmail",
"type": "string",
"value": "user@example.com "
}
]
}
},
"typeVersion": 3.4
},
{
"id": "a6750cc6-5f3b-4b81-9ef0-5907ec1a2caa",
"name": "PageSpeed Mobile",
"type": "n8n-nodes-base.httpRequest",
"notes": "Google PageSpeed Insights, strategy=mobile. Auth: httpQueryAuth credential must send the API key as the \"key\" query param.",
"maxTries": 3,
"position": [
560,
0
],
"parameters": {
"url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed",
"options": {
"timeout": 60000
},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpQueryAuth",
"queryParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $('Config').item.json.url }}"
},
{
"name": "strategy",
"value": "mobile"
},
{
"name": "category",
"value": "performance"
}
]
}
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "148eb3c1-042d-4b1c-859d-3718723b03fe",
"name": "Combine Mobile + Desktop",
"type": "n8n-nodes-base.merge",
"position": [
784,
96
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "63136687-de2a-45c6-a516-fa4f4dc36c48",
"name": "PageSpeed Desktop",
"type": "n8n-nodes-base.httpRequest",
"notes": "Google PageSpeed Insights, strategy=desktop. Same httpQueryAuth credential as the Mobile node.",
"maxTries": 3,
"position": [
560,
192
],
"parameters": {
"url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed",
"options": {
"timeout": 60000
},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpQueryAuth",
"queryParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $('Config').item.json.url }}"
},
{
"name": "strategy",
"value": "desktop"
},
{
"name": "category",
"value": "performance"
}
]
}
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 5000
},
{
"id": "70ffc62d-a243-4676-b271-ec49496f9565",
"name": "Build Report",
"type": "n8n-nodes-base.code",
"position": [
1024,
96
],
"parameters": {
"jsCode": "const cfg = $('Config').first().json;\nconst url = cfg.url || '';\nconst recipient = cfg.recipientEmail || '';\nlet host = url;\ntry { host = url.replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '').replace(/^www\\./, '') || url; } catch (e) {}\n\nconst mobileResp = $('PageSpeed Mobile').first().json;\nconst desktopResp = $('PageSpeed Desktop').first().json;\n\nfunction extract(resp) {\n resp = resp || {};\n const le = (resp.loadingExperience && resp.loadingExperience.metrics) || {};\n const lh = resp.lighthouseResult || {};\n const audits = lh.audits || {};\n const cats = lh.categories || {};\n const perfScore = (cats.performance && typeof cats.performance.score === 'number') ? Math.round(cats.performance.score * 100) : null;\n const fLcp = le.LARGEST_CONTENTFUL_PAINT_MS ? le.LARGEST_CONTENTFUL_PAINT_MS.percentile : null;\n const fInp = le.INTERACTION_TO_NEXT_PAINT ? le.INTERACTION_TO_NEXT_PAINT.percentile : (le.EXPERIMENTAL_INTERACTION_TO_NEXT_PAINT ? le.EXPERIMENTAL_INTERACTION_TO_NEXT_PAINT.percentile : null);\n const fClsRaw = le.CUMULATIVE_LAYOUT_SHIFT_SCORE ? le.CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile : null;\n const fCls = (fClsRaw !== null && fClsRaw !== undefined) ? fClsRaw / 100 : null;\n const labLcp = audits['largest-contentful-paint'] ? audits['largest-contentful-paint'].numericValue : null;\n const labCls = audits['cumulative-layout-shift'] ? audits['cumulative-layout-shift'].numericValue : null;\n const labTbt = audits['total-blocking-time'] ? audits['total-blocking-time'].numericValue : null;\n const hasField = (fLcp !== null && fLcp !== undefined);\n return {\n hasField: hasField,\n lcp: (fLcp !== null && fLcp !== undefined) ? fLcp : labLcp,\n inp: (fInp !== null && fInp !== undefined) ? fInp : null,\n tbt: labTbt,\n cls: (fCls !== null && fCls !== undefined) ? fCls : labCls,\n perfScore: perfScore\n };\n}\n\nconst m = extract(mobileResp);\nconst d = extract(desktopResp);\n\nfunction stLcp(v){ if(v===null||v===undefined) return null; if(v<=2500) return 'good'; if(v<=4000) return 'ni'; return 'poor'; }\nfunction stInp(v){ if(v===null||v===undefined) return null; if(v<=200) return 'good'; if(v<=500) return 'ni'; return 'poor'; }\nfunction stCls(v){ if(v===null||v===undefined) return null; if(v<=0.1) return 'good'; if(v<=0.25) return 'ni'; return 'poor'; }\nfunction stScore(v){ if(v===null||v===undefined) return null; if(v>=90) return 'good'; if(v>=50) return 'ni'; return 'poor'; }\n\nconst META = {\n good: { color: '#16a34a', bg: '#dcfce7', label: 'Good', dot: '\ud83d\udfe2' },\n ni: { color: '#b45309', bg: '#fef3c7', label: 'Needs work', dot: '\ud83d\udfe1' },\n poor: { color: '#dc2626', bg: '#fee2e2', label: 'Poor', dot: '\ud83d\udd34' },\n none: { color: '#6b7280', bg: '#f3f4f6', label: 'No data', dot: '\u26aa' }\n};\n\nfunction fmtLcp(v){ return (v===null||v===undefined) ? '\u2014' : (v/1000).toFixed(2) + ' s'; }\nfunction fmtInp(v){ return (v===null||v===undefined) ? '\u2014' : Math.round(v) + ' ms'; }\nfunction fmtCls(v){ return (v===null||v===undefined) ? '\u2014' : v.toFixed(3); }\nfunction fmtScore(v){ return (v===null||v===undefined) ? '\u2014' : String(v); }\n\nfunction cell(valueStr, status){\n const meta = META[status || 'none'];\n return '<td style=\"padding:10px 14px;text-align:center;border-bottom:1px solid #e5e7eb;\">' +\n '<span style=\"display:inline-block;padding:4px 10px;border-radius:9999px;background:' + meta.bg + ';color:' + meta.color + ';font-weight:600;font-size:13px;\">' +\n meta.dot + ' ' + valueStr + '</span></td>';\n}\n\nfunction tr(metric, target, mVal, mSt, dVal, dSt){\n return '<tr>' +\n '<td style=\"padding:10px 14px;border-bottom:1px solid #e5e7eb;font-weight:600;color:#111827;\">' + metric +\n '<div style=\"font-weight:400;color:#6b7280;font-size:12px;\">' + target + '</div></td>' +\n cell(mVal, mSt) + cell(dVal, dSt) + '</tr>';\n}\n\nconst statuses = [stLcp(m.lcp),stInp(m.inp),stCls(m.cls),stScore(m.perfScore),stLcp(d.lcp),stInp(d.inp),stCls(d.cls),stScore(d.perfScore)];\nlet overall = 'good';\nfor (let i=0;i<statuses.length;i++){ const s=statuses[i]; if(s==='poor'){ overall='poor'; break; } if(s==='ni' && overall!=='poor'){ overall='ni'; } }\nconst overallDot = META[overall].dot;\nconst dateStr = $now.toFormat('dd LLL yyyy');\n\nconst rowsHtml =\n tr('Largest Contentful Paint (LCP)','Good \u2264 2.5s', fmtLcp(m.lcp), stLcp(m.lcp), fmtLcp(d.lcp), stLcp(d.lcp)) +\n tr('Interaction to Next Paint (INP)','Good \u2264 200ms', fmtInp(m.inp), stInp(m.inp), fmtInp(d.inp), stInp(d.inp)) +\n tr('Cumulative Layout Shift (CLS)','Good \u2264 0.1', fmtCls(m.cls), stCls(m.cls), fmtCls(d.cls), stCls(d.cls)) +\n tr('Performance Score','Good \u2265 90', fmtScore(m.perfScore), stScore(m.perfScore), fmtScore(d.perfScore), stScore(d.perfScore));\n\nconst fieldNote = (!m.hasField && !d.hasField)\n ? '<p style=\"color:#6b7280;font-size:12px;margin:8px 0 0;\">No real-user (CrUX) field data was available for this URL, so lab metrics are shown. INP is a field-only metric and shows as a dash for lab data.</p>'\n : '<p style=\"color:#6b7280;font-size:12px;margin:8px 0 0;\">Values reflect real-user (CrUX) field data where available, otherwise Lighthouse lab data.</p>';\n\nconst html =\n'<div style=\"font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:640px;margin:0 auto;color:#111827;\">' +\n '<h2 style=\"margin:0 0 4px;\">' + overallDot + ' Core Web Vitals \u2014 ' + host + '</h2>' +\n '<p style=\"margin:0 0 16px;color:#6b7280;font-size:14px;\">Daily report \u00b7 ' + dateStr + '</p>' +\n '<table style=\"width:100%;border-collapse:collapse;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;font-size:14px;\">' +\n '<thead><tr style=\"background:#f9fafb;\">' +\n '<th style=\"padding:10px 14px;text-align:left;border-bottom:1px solid #e5e7eb;\">Metric</th>' +\n '<th style=\"padding:10px 14px;text-align:center;border-bottom:1px solid #e5e7eb;\">\ud83d\udcf1 Mobile</th>' +\n '<th style=\"padding:10px 14px;text-align:center;border-bottom:1px solid #e5e7eb;\">\ud83d\udcbb Desktop</th>' +\n '</tr></thead><tbody>' + rowsHtml + '</tbody></table>' +\n fieldNote +\n '<p style=\"margin:16px 0 0;\"><a href=\"https://pagespeed.web.dev/analysis?url=' + encodeURIComponent(url) + '\" style=\"color:#2563eb;\">View full PageSpeed report</a></p>' +\n'</div>';\n\nconst subject = overallDot + ' Core Web Vitals \u2014 ' + host + ' \u2014 Mobile ' + fmtScore(m.perfScore) + '/100, Desktop ' + fmtScore(d.perfScore) + '/100';\n\nreturn [{ json: { subject: subject, html: html, recipient: recipient, host: host, mobile: m, desktop: d } }];"
},
"typeVersion": 2
},
{
"id": "f5b702d6-94ce-44d5-8147-649f6103d134",
"name": "Send Report Email",
"type": "n8n-nodes-base.gmail",
"maxTries": 3,
"position": [
1248,
96
],
"parameters": {
"sendTo": "={{ $json.recipient.trim() }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false
},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 2.2,
"waitBetweenTries": 5000
},
{
"id": "1e3f3b4c-a64c-4a36-817a-898382c44959",
"name": "Sticky Note b896b6de",
"type": "n8n-nodes-base.stickyNote",
"position": [
-48,
-144
],
"parameters": {
"color": 7,
"width": 444,
"height": 440,
"content": "## \u2699\ufe0f Edit these\nSet the **website URL** to audit and the **recipient email**. Nothing else needs changing to run.\n\nEdit schedule node as needed"
},
"typeVersion": 1
},
{
"id": "563580fe-c9a6-4b73-a161-5fd6c12dd890",
"name": "Sticky Note 0273566e",
"type": "n8n-nodes-base.stickyNote",
"position": [
416,
-208
],
"parameters": {
"color": 7,
"width": 508,
"height": 568,
"content": "## PageSpeed Insights \u2014 Mobile & Desktop\nQueries Google PageSpeed for real-user (CrUX) + lab metrics on both strategies, then merges the two responses."
},
"typeVersion": 1
},
{
"id": "948587f5-b17f-4a77-96dd-78fdb7cfdbd6",
"name": "Sticky Note b68be57c",
"type": "n8n-nodes-base.stickyNote",
"position": [
944,
-144
],
"parameters": {
"color": 7,
"width": 508,
"height": 456,
"content": "## Build & email the report\nClassifies LCP / INP / CLS + performance score against Google thresholds and emails a color-coded Mobile-vs-Desktop table."
},
"typeVersion": 1
},
{
"id": "3ffb3bf7-02bb-4142-a2cb-5d68e70847f9",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-656,
-656
],
"parameters": {
"width": 576,
"height": 1356,
"content": "# \ud83d\udcca Daily Core Web Vitals Monitor \u2014 Mobile & Desktop \u2192 Gmail\n\nEvery morning this workflow checks a page's **Core Web Vitals** on both **mobile**\nand **desktop** via **Google PageSpeed Insights**, then emails a clean, color-coded\n\ud83d\udfe2\ud83d\udfe1\ud83d\udd34 report. No spreadsheets \u2014 your inbox thread is the history.\n\n## \u2699\ufe0f How it works\n1. **Every Day 7 AM** \u2014 schedule trigger fires daily at 07:00.\n2. **Config** \u2014 you set the **website URL** and the **recipient email** here.\n This is the only node you need to edit.\n3. **PageSpeed Mobile** + **PageSpeed Desktop** \u2014 two HTTP calls to Google\n PageSpeed Insights (strategy = mobile / desktop), sharing one API-key credential.\n4. **Combine Mobile + Desktop** \u2014 merges the two responses into one.\n5. **Build Report** \u2014 grades LCP / INP / CLS + performance score against Google's\n official thresholds and builds a color-coded HTML table.\n6. **Send Report Email** \u2014 Gmail delivers the report to you.\n\n## \ud83d\udee0\ufe0f Setup (~5 min)\n1. **Google Cloud** \u2192 enable the **PageSpeed Insights API** and create an **API key**.\n2. **Create an `httpQueryAuth` credential**:\n \u2022 Query-param **Name** = `key`\n \u2022 **Value** = your PageSpeed API key\n Select it on both **PageSpeed Mobile** and **PageSpeed Desktop**.\n3. **Select your Gmail (OAuth2) credential** on **Send Report Email**.\n4. **Config** node \u2192 set `url` (the page to audit) and `recipientEmail`.\n5. **Publish / activate** \u2192 it runs automatically every day at 07:00.\n\n## \ud83d\udcca What the report shows\nEach metric, Mobile vs Desktop, graded to Google's thresholds:\n\ud83d\udfe2 **LCP** \u2264 2.5s \ud83d\udfe1 \u2264 4s \ud83d\udd34 > 4s\n\ud83d\udfe2 **INP** \u2264 200ms \ud83d\udfe1 \u2264 500ms \ud83d\udd34 > 500ms\n\ud83d\udfe2 **CLS** \u2264 0.1 \ud83d\udfe1 \u2264 0.25 \ud83d\udd34 > 0.25\n\ud83d\udfe2 **Performance Score** \u2265 90 \ud83d\udfe1 \u2265 50 \ud83d\udd34 < 50\n\n## \ud83d\udcdd Good to know\n\u2022 Uses **real-user (CrUX) field data** when the URL has enough traffic; otherwise\n falls back to **Lighthouse lab** data.\n\u2022 **INP** is a field-only metric \u2014 on lab-only URLs it shows as \u201c\u2014\u201d.\n\u2022 Change the cadence in the **Every Day 7 AM** node (hourly, weekly, custom cron\u2026).\n\u2022 Both PageSpeed calls and Gmail **retry 3\u00d7** on transient failures; PageSpeed has a\n 60s timeout because its analysis can be slow.\n\u2022 **Gmail-only by design** \u2014 search your inbox thread to see history / trends over time."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "f5b7e8b1-a528-4898-9d1c-f4e2ba49718b",
"nodeGroups": [],
"connections": {
"Config": {
"main": [
[
{
"node": "PageSpeed Mobile",
"type": "main",
"index": 0
},
{
"node": "PageSpeed Desktop",
"type": "main",
"index": 0
}
]
]
},
"Build Report": {
"main": [
[
{
"node": "Send Report Email",
"type": "main",
"index": 0
}
]
]
},
"Every Day 7 AM": {
"main": [
[
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"PageSpeed Mobile": {
"main": [
[
{
"node": "Combine Mobile + Desktop",
"type": "main",
"index": 0
}
]
]
},
"PageSpeed Desktop": {
"main": [
[
{
"node": "Combine Mobile + Desktop",
"type": "main",
"index": 1
}
]
]
},
"Combine Mobile + Desktop": {
"main": [
[
{
"node": "Build 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.
gmailOAuth2httpQueryAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs daily at 7 AM, checks a URL’s Core Web Vitals and performance for mobile and desktop using the Google PageSpeed Insights API, then builds a color-coded HTML report and emails it to a recipient via Gmail. Runs every day at 7:00 AM on a schedule. Loads the…
Source: https://n8n.io/workflows/17796/ — 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.
Relatórios aos Laboratórios. Uses httpRequest, gmail, formTrigger. Scheduled trigger; 54 nodes.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
14310 Send Overdue Invoice Payment Reminders With Ifirma Gmail Postgrid And Slack. Uses httpRequest, stopAndError, slack, gmail. Scheduled trigger; 53 nodes.
Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.
Addendo — Blog Automatico Don Jacinto Nahual. Uses httpRequest, redis, github, gmail. Scheduled trigger; 51 nodes.