{
  "id": "8DUJ1ZRkEIWd8IMd",
  "name": "Send weekly Vercel analytics digest emails with AI summaries from Groq",
  "tags": [],
  "nodes": [
    {
      "id": "sticky-main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -16
      ],
      "parameters": {
        "width": 520,
        "height": 1056,
        "content": "## Send weekly Vercel analytics digest emails with AI summaries from Groq\n\n### Who's it for\n\nAnyone hosting a website on Vercel who wants a weekly analytics summary in their inbox instead of checking dashboards.\n\n### How it works\n\n1. Triggers every Monday at 9 AM.\n2. Fetches the last 7 days from Vercel Web Analytics \u2014 summary metrics, top pages, and referrer sources.\n3. A Code node consolidates the three API responses into a single report object.\n4. An AI agent (Groq Llama 3.3 70B) formats it into a clean HTML email digest.\n5. If the AI call ever fails, a fallback branch sends the raw statistics instead \u2014 you never miss a digest.\n6. The digest is delivered to your inbox via SMTP.\n\n### Setup steps\n\n- [ ] Create a Vercel API token: Vercel Dashboard \u2192 Settings \u2192 Tokens\n- [ ] In n8n, create a **Header Auth** credential: Name = `Authorization`, Value = `Bearer YOUR_TOKEN`\n- [ ] Enter your **Vercel Project ID**, **Team ID** (leave blank if personal), **site name**, and **email addresses** in the Set Config node\n- [ ] Connect your **Groq** credential (free tier works)\n- [ ] Connect your **SMTP** credential\n\n### Customisation\n\nAdjust the schedule, change the date range in Set Config, swap Groq for another LLM, or add country/device breakdowns by adding more Fetch nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-schedule",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 400,
        "content": "## Schedule\n\nFires every Monday at 9 AM (your n8n instance timezone).\n\nAdjust the day and time in the trigger node to suit your preference."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-configure",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 256,
        "height": 480,
        "content": "## Configure\n\nThe only node you must edit. Set your Vercel **Project ID**, **Team ID** (blank if personal), **site name**, and **email addresses** here.\n\nFind Project ID in:\nVercel Dashboard \u2192 Project \u2192 Settings \u2192 General."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-fetch",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 480,
        "content": "## Fetch Vercel Analytics Data (Last 7 Days)\n\nThree sequential GET requests to the Vercel Web Analytics Query API:\n- **Summary** \u2014 `visits/count` \u2192 total page views + unique visitors\n- **Top Pages** \u2014 `visits/aggregate` grouped by `requestPath`, top 5\n- **Referrers** \u2014 `visits/aggregate` grouped by `referrerHostname`, top 5\n\n**Good to know:** these are the only endpoints the API exposes \u2014 there are no /summary, /pages or /referrers endpoints, and only pageviews + visitors metrics exist (no bounce rate or duration). All requests use millisecond timestamps (`since`/`until`) from Set Config. Auth via Header Auth credential (Bearer token)."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-process",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 760,
        "content": "## Process & Send\n\n**Build Report** \u2014 consolidates all three Vercel API responses into one clean JSON summary with defensive parsing (handles empty or missing data gracefully).\n\n**Digest Agent** (Groq Llama 3.3 70B) \u2014 formats the data into a structured HTML email with headline, key metrics, top pages, referrer sources, and one actionable insight.\n\n**Raw Stats Fallback** \u2014 if the AI call errors, this branch turns the report into a plain-stats email body, so the digest still arrives.\n\n**Send Digest Email** \u2014 wraps the output in a clean HTML template and delivers it via SMTP."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "Every Monday 9 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        480,
        368
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "set-config",
      "name": "Set Config",
      "type": "n8n-nodes-base.set",
      "position": [
        720,
        368
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "project-id",
              "name": "projectId",
              "type": "string",
              "value": "YOUR_VERCEL_PROJECT_ID"
            },
            {
              "id": "team-id",
              "name": "teamId",
              "type": "string",
              "value": ""
            },
            {
              "id": "site-name",
              "name": "siteName",
              "type": "string",
              "value": "My Website"
            },
            {
              "id": "recipient-email",
              "name": "recipientEmail",
              "type": "string",
              "value": "you@example.com"
            },
            {
              "id": "sender-email",
              "name": "senderEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "from-ts",
              "name": "since",
              "type": "number",
              "value": "={{ $now.minus({ days: 7 }).toMillis() }}"
            },
            {
              "id": "to-ts",
              "name": "until",
              "type": "number",
              "value": "={{ $now.toMillis() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fetch-summary",
      "name": "Fetch Vercel Summary",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        992,
        368
      ],
      "parameters": {
        "url": "=https://api.vercel.com/v1/query/web-analytics/visits/count?projectId={{ $('Set Config').item.json.projectId }}{{ $('Set Config').item.json.teamId ? '&teamId=' + $('Set Config').item.json.teamId : '' }}&since={{ $('Set Config').item.json.since }}&until={{ $('Set Config').item.json.until }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "fetch-pages",
      "name": "Fetch Vercel Top Pages",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1200,
        368
      ],
      "parameters": {
        "url": "=https://api.vercel.com/v1/query/web-analytics/visits/aggregate?projectId={{ $('Set Config').item.json.projectId }}{{ $('Set Config').item.json.teamId ? '&teamId=' + $('Set Config').item.json.teamId : '' }}&since={{ $('Set Config').item.json.since }}&until={{ $('Set Config').item.json.until }}&by=requestPath&limit=5",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "fetch-referrers",
      "name": "Fetch Vercel Referrers",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1440,
        368
      ],
      "parameters": {
        "url": "=https://api.vercel.com/v1/query/web-analytics/visits/aggregate?projectId={{ $('Set Config').item.json.projectId }}{{ $('Set Config').item.json.teamId ? '&teamId=' + $('Set Config').item.json.teamId : '' }}&since={{ $('Set Config').item.json.since }}&until={{ $('Set Config').item.json.until }}&by=referrerHostname&limit=5",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "build-report",
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        368
      ],
      "parameters": {
        "jsCode": "const summaryRaw   = $('Fetch Vercel Summary').item.json;\nconst pagesRaw     = $('Fetch Vercel Top Pages').item.json;\nconst referrersRaw = $('Fetch Vercel Referrers').item.json;\n\n// visits/count \u2192 { data: { pageviews, visitors } }\nconst s = summaryRaw.data || {};\nconst pageViews = s.pageviews || 0;\nconst visitors  = s.visitors  || 0;\nconst pagesPerVisit = visitors > 0 ? (pageViews / visitors).toFixed(2) : '0.00';\n\n// visits/aggregate by=requestPath \u2192 { data: [{ requestPath, pageviews, visitors }] }\nconst pagesArr = Array.isArray(pagesRaw.data) ? pagesRaw.data : [];\nconst topPages = pagesArr.slice(0, 5).map(p => ({\n  page:     p.requestPath || 'Unknown',\n  views:    p.pageviews || 0,\n  visitors: p.visitors  || 0\n}));\n\n// visits/aggregate by=referrerHostname \u2192 { data: [{ referrerHostname, pageviews, visitors }] }\nconst refArr = Array.isArray(referrersRaw.data) ? referrersRaw.data : [];\nconst referrers = refArr.slice(0, 5).map(r => ({\n  source: r.referrerHostname || 'Direct / None',\n  visits: r.visitors || r.pageviews || 0\n}));\n\nconst reportData = JSON.stringify({\n  period: 'Last 7 days',\n  overview: { pageViews, visitors, pagesPerVisit },\n  topPages,\n  referrers\n}, null, 2);\n\nreturn [{ json: { reportData } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "digest-agent",
      "name": "Digest Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueErrorOutput",
      "position": [
        1920,
        368
      ],
      "parameters": {
        "text": "={{ $json.reportData }}",
        "options": {
          "systemMessage": "You are a weekly analytics reporter for a website.\n\nAnalyse the Vercel Analytics JSON data provided and generate a concise weekly digest email body.\n\nOutput clean HTML only \u2014 no <html>, <head>, or <body> tags. Use:\n- <h3 style=\"color:#1a73c8;margin:20px 0 8px;\"> for section headers\n- <p style=\"margin:0 0 12px;\"> for paragraphs\n- <ul style=\"margin:0 0 12px;padding-left:20px;\"><li> for list items\n- <strong> for key numbers and metrics\n\nInclude exactly these sections:\n1. <h3>\ud83d\udcca Weekly Headline</h3> \u2014 one sentence highlighting the most notable stat (e.g. '1 200 page views this week')\n2. <h3>\ud83d\udcc8 Key Metrics</h3> \u2014 page views, unique visitors, pages per visit\n3. <h3>\ud83d\udd1d Top Pages</h3> \u2014 page path and view count for each\n4. <h3>\ud83d\udea6 Traffic Sources</h3> \u2014 referrer hostname and visit count ('Direct / None' means direct traffic)\n5. <h3>\ud83d\udca1 Insight</h3> \u2014 one specific, actionable observation the site owner should act on\n\nOnly report metrics present in the data \u2014 never invent numbers. Keep total under 300 words. Tone: professional, data-driven, and friendly."
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "groq-model",
      "name": "Groq Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        1856,
        640
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {}
      },
      "credentials": {
        "groqApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "raw-stats-fallback",
      "name": "Raw Stats Fallback",
      "type": "n8n-nodes-base.code",
      "position": [
        2112,
        560
      ],
      "parameters": {
        "jsCode": "// Runs only when the AI call fails \u2014 builds a plain-stats email body\n// with the same HTML styling and the same `output` field the email node reads.\nconst report = JSON.parse($('Build Report').first().json.reportData);\nconst o = report.overview || {};\n\nconst pageRows = (report.topPages || [])\n  .map(p => `<li><strong>${p.page}</strong> \u2014 ${p.views} views (${p.visitors} visitors)</li>`)\n  .join('');\nconst refRows = (report.referrers || [])\n  .map(r => `<li><strong>${r.source}</strong> \u2014 ${r.visits} visits</li>`)\n  .join('');\n\nconst output = `\n<p style=\"margin:0 0 12px;\">The AI summary was unavailable this week, so here are your raw numbers.</p>\n<h3 style=\"color:#1a73c8;margin:20px 0 8px;\">\ud83d\udcc8 Key Metrics</h3>\n<ul style=\"margin:0 0 12px;padding-left:20px;\">\n  <li>Page views: <strong>${o.pageViews ?? 0}</strong></li>\n  <li>Unique visitors: <strong>${o.visitors ?? 0}</strong></li>\n  <li>Pages per visit: <strong>${o.pagesPerVisit ?? '0.00'}</strong></li>\n</ul>\n<h3 style=\"color:#1a73c8;margin:20px 0 8px;\">\ud83d\udd1d Top Pages</h3>\n<ul style=\"margin:0 0 12px;padding-left:20px;\">${pageRows || '<li>No data this week</li>'}</ul>\n<h3 style=\"color:#1a73c8;margin:20px 0 8px;\">\ud83d\udea6 Traffic Sources</h3>\n<ul style=\"margin:0 0 12px;padding-left:20px;\">${refRows || '<li>No data this week</li>'}</ul>`;\n\nreturn [{ json: { output } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "send-email",
      "name": "Send Digest Email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        2272,
        368
      ],
      "parameters": {
        "html": "=<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#f0f4f8;font-family:'Segoe UI',Arial,sans-serif;\">\n<table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:#f0f4f8;padding:32px 16px;\">\n<tr><td align=\"center\">\n<table role=\"presentation\" width=\"600\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:#ffffff;border-radius:12px;overflow:hidden;border:1px solid #e2e8f0;\">\n  <tr>\n    <td style=\"background:linear-gradient(135deg,#0f4c81 0%,#1a73c8 100%);padding:24px 36px;\">\n      <p style=\"margin:0;font-size:20px;font-weight:700;color:#ffffff;\">\ud83d\udcca Weekly Analytics Digest \u2014 {{ $('Set Config').item.json.siteName }}</p>\n      <p style=\"margin:6px 0 0;font-size:13px;color:rgba(255,255,255,0.75);\">Week ending {{ $now.minus({days: 1}).toFormat('MMMM d, yyyy') }}</p>\n    </td>\n  </tr>\n  <tr>\n    <td style=\"padding:28px 36px;color:#374151;font-size:14px;line-height:1.7;\">\n      {{ $json.output }}\n    </td>\n  </tr>\n  <tr>\n    <td style=\"background:#f8fafc;border-top:1px solid #e2e8f0;padding:14px 36px;text-align:center;font-size:11px;color:#9ca3af;\">\n      Automated with n8n\n    </td>\n  </tr>\n</table>\n</td></tr>\n</table>\n</body>\n</html>",
        "options": {},
        "subject": "=Weekly analytics digest for {{ $('Set Config').item.json.siteName }} \u2014 w/e {{ $now.minus({days: 1}).toFormat('MMM d, yyyy') }}",
        "toEmail": "={{ $('Set Config').item.json.recipientEmail }}",
        "fromEmail": "={{ $('Set Config').item.json.senderEmail }}"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "9664ab5f-30fc-4034-b6f5-c3426ff40a1d",
  "nodeGroups": [],
  "connections": {
    "Groq Model": {
      "ai_languageModel": [
        [
          {
            "node": "Digest Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Set Config": {
      "main": [
        [
          {
            "node": "Fetch Vercel Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          {
            "node": "Digest Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Digest Agent": {
      "main": [
        [
          {
            "node": "Send Digest Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Raw Stats Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Monday 9 AM": {
      "main": [
        [
          {
            "node": "Set Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Raw Stats Fallback": {
      "main": [
        [
          {
            "node": "Send Digest Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Vercel Summary": {
      "main": [
        [
          {
            "node": "Fetch Vercel Top Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Vercel Referrers": {
      "main": [
        [
          {
            "node": "Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Vercel Top Pages": {
      "main": [
        [
          {
            "node": "Fetch Vercel Referrers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}