{
  "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
          }
        ]
      ]
    }
  }
}