AutomationFlowsMarketing & Ads › Optimize Klaviyo Campaign Send Times and Email Reports with Gmail

Optimize Klaviyo Campaign Send Times and Email Reports with Gmail

ByAfeez @tundek on n8n.io

How it works

Cron / scheduled trigger★★★★☆ complexity16 nodesHTTP RequestGmailError Trigger
Marketing & Ads Trigger: Cron / scheduled Nodes: 16 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #15552 — we link there as the canonical source.

This workflow follows the Error Trigger → Gmail 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 →

Download .json
{
  "id": "6tqe8rVRa6LNgmPa",
  "name": "Campaign Send-Time Optimizer",
  "tags": [],
  "nodes": [
    {
      "id": "013ab571-615d-4a19-badb-7e4f72eb402b",
      "name": "\ud83d\udcca Send-Time Optimizer \u2014 Main",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1072,
        288
      ],
      "parameters": {
        "color": 4,
        "width": 900,
        "height": 484,
        "content": "## \ud83d\udcca Campaign Send-Time Optimizer\n\n**Goal:** Analyse your Klaviyo email campaign history to identify which send times produce the highest open and click rates per audience segment. Delivers a ranked HTML report every Monday at 8:00 AM.\n\n### How it works\n1. **Schedule** fires every Monday at 8 AM (or trigger manually for on-demand runs).\n2. **Config** holds the central settings \u2014 lookback window (90 days) and minimum campaigns per slot (3).\n3. **Get Sent Campaigns** paginates the Klaviyo Campaigns API to pull every email campaign.\n4. **Flatten Campaigns** filters to Sent status within the window, extracts the UTC send hour, and builds an audience-segment key.\n5. **Fetch Lists** retrieves all list names for human-readable labels in the final report.\n6. **Get Campaign Stats** posts a single bulk request to the campaign-values-reports endpoint and returns open + click rates.\n7. **Aggregate by Hour** joins metadata with stats, groups into (audience \u00d7 hour) buckets, and averages each.\n8. **Format Email Report** ranks segments by best open rate, adds medal icons for the top 3 slots per segment.\n9. **Gmail \u2014 Send Report** delivers the styled HTML email to your configured inbox.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5e33f838-4a9b-4b45-891d-d3a3d340334b",
      "name": "\u26a0\ufe0f Critical Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1072,
        864
      ],
      "parameters": {
        "color": 2,
        "width": 940,
        "height": 580,
        "content": "## \u26a0\ufe0f Critical \u2014 Do before activation\n\n**The workflow will fail or send to the wrong recipient if you skip these:**\n\n### \ud83d\udd11 Credentials\n- **Klaviyo API Key** \u2014 required on three HTTP Request nodes (`Get Sent Campaigns`, `Fetch Lists`, `Get Campaign Stats`). Generate at *Klaviyo \u2192 Account \u2192 Settings \u2192 API Keys*. Use a Private API Key with `campaigns:read` + `lists:read` + `metrics:read` scopes.\n- **Gmail OAuth2** \u2014 required on both Gmail nodes. The Error Alert uses a separate credential from the report sender so a failure in the sender's auth doesn't suppress its own error message.\n\n### \ud83d\udce7 Recipient addresses\n- **Gmail \u2014 Send Report** \u2192 currently `user@example.com`. Update to your inbox.\n- **Gmail \u2014 Error Alert** \u2192 currently `user@example.com`. Update to your monitoring inbox (can be same as above).\n\n### \ud83c\udfaf conversion_metric_id\nThe body of **Get Campaign Stats** hardcodes `X7ghUW` (Klaviyo's default Shopify *Placed Order* metric). If your store uses Shopify, you're fine. If not, replace with your account's primary conversion metric ID \u2014 find it at *Klaviyo \u2192 Account \u2192 Settings \u2192 Custom Metrics*.\n\n### \u23f1\ufe0f Timezone\nAll times in the final report are **UTC** by design. If you want local time, edit the `getUTCHours()` call in *Flatten Campaigns* and the label format in *Aggregate by Hour*."
      },
      "typeVersion": 1
    },
    {
      "id": "7ffc952b-9d7d-40b7-b956-94b962f6ee00",
      "name": "Section \u2014 Setup & Klaviyo Data Pull",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 1116,
        "height": 572,
        "content": "### \ud83d\udce5 Setup & Klaviyo Data Pull\n\nTriggers on the weekly schedule, loads the central config (lookback window + confidence threshold), then makes three Klaviyo API calls in sequence \u2014 first to fetch all email campaigns with cursor pagination, then to fetch list names for readable labels, finally a bulk POST to the campaign-values-reports endpoint for open and click rates."
      },
      "typeVersion": 1
    },
    {
      "id": "45c48c39-1d93-4c67-8305-6010a725dbe5",
      "name": "Section \u2014 Analysis & Report Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 728,
        "height": 428,
        "content": "### \ud83d\udce4 Analysis & Report Delivery\n\nJoins campaign metadata with stats, groups them into (audience \u00d7 send_hour) buckets and averages each metric. Slots with fewer than 3 campaigns are flagged as statistically thin. The formatter ranks each segment by best open rate, awards medal icons to the top 3 slots, and the Gmail node delivers the styled HTML report."
      },
      "typeVersion": 1
    },
    {
      "id": "c96436f7-2d1b-49b6-8585-3205cdc74c75",
      "name": "Schedule \u2014 Weekly Monday",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "Entry point. Fires every Monday at 8:00 AM automatically. Trigger manually from the canvas to run on demand at any time.",
      "position": [
        64,
        384
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "039b5965-30f3-43cc-bc0d-39012a85fa06",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "notes": "Central configuration. Edit lookback_days (default 90) or min_campaigns_per_slot (default 3) here to adjust the analysis window and confidence threshold without touching any code nodes.",
      "position": [
        288,
        384
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-1",
              "name": "lookback_days",
              "type": "number",
              "value": 90
            },
            {
              "id": "cfg-2",
              "name": "min_campaigns_per_slot",
              "type": "number",
              "value": 3
            },
            {
              "id": "cfg-3",
              "name": "start_date",
              "type": "string",
              "value": "={{ $now.minus({days: 90}).toUTC().toISO() }}"
            },
            {
              "id": "cfg-4",
              "name": "report_date",
              "type": "string",
              "value": "={{ $now.toFormat('MMMM d, yyyy') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "26e958bc-b1f5-4cce-ae24-bb363e0c6e88",
      "name": "Get Sent Campaigns",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Klaviyo Campaigns API \u2014 fetches all email campaigns using cursor-based pagination (follows links.next automatically, up to 20 pages). Filters to email channel at the API level. Requires: Klaviyo API Key credential.",
      "position": [
        512,
        384
      ],
      "parameters": {
        "url": "={{ 'https://a.klaviyo.com/api/campaigns/?fields%5Bcampaign%5D=name,scheduled_at,send_strategy,audiences,status&filter=' + encodeURIComponent(\"equals(messages.channel,'email')\") }}",
        "options": {
          "pagination": {}
        },
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "revision",
              "value": "2024-02-15"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3bd90a46-07dd-4a59-9f34-a6515dea299b",
      "name": "Flatten Campaigns",
      "type": "n8n-nodes-base.code",
      "notes": "Parses the paginated Klaviyo response (handles n8n double-serialization). Filters to status=Sent campaigns within the 90-day window. Extracts UTC send hour from scheduled_at. Builds audience segment key from included list IDs. Throws a descriptive error if no campaigns pass the filter.",
      "position": [
        736,
        384
      ],
      "parameters": {
        "jsCode": "// Collect all pages returned by the paginating HTTP Request node,\n// filter to Sent email campaigns within the last 90 days,\n// and extract the send hour (UTC) and segment key from each.\n\nconst allItems = $input.all();\nconst cutoffMs = Date.now() - 90 * 24 * 60 * 60 * 1000;\n\nconst campaigns = [];\n\nfor (const item of allItems) {\n  const page = item.json;\n\n  let parsed = page;\n  if (typeof page.data === 'string') {\n    try { parsed = JSON.parse(page.data); } catch (e) { parsed = page; }\n  }\n\n  let rows;\n  if (Array.isArray(parsed)) {\n    rows = parsed;\n  } else if (Array.isArray(parsed.data)) {\n    rows = parsed.data;\n  } else {\n    rows = [];\n  }\n\n  for (const row of rows) {\n    const attrs = row.attributes || {};\n    const status = (attrs.status || '').toLowerCase();\n    if (status !== 'sent') continue;\n\n    const dateStr = attrs.scheduled_at || attrs.send_strategy?.options_static?.datetime || '';\n    const sentDate = dateStr ? new Date(dateStr) : null;\n    if (sentDate && !isNaN(sentDate.getTime()) && sentDate.getTime() < cutoffMs) continue;\n    const sendHour = (sentDate && !isNaN(sentDate.getTime())) ? sentDate.getUTCHours() : 9;\n\n    const audiences = attrs.audiences || {};\n    const listIds = audiences.included || [];\n    const segmentKey = listIds.length > 0 ? listIds.join(',') : 'all';\n\n    campaigns.push({id: row.id, name: attrs.name || 'Unknown', send_hour: sendHour, segment_key: segmentKey, scheduled_at: dateStr});\n  }\n}\n\nif (campaigns.length === 0) {\n  const firstPage = allItems[0]?.json || {};\n  throw new Error(`No sent campaigns found after parsing. Items received: ${allItems.length}. First item keys: ${Object.keys(firstPage).join(', ')}. data type: ${typeof firstPage.data}`);\n}\n\nreturn [{ json: { campaigns, total: campaigns.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "83107e6c-6dc5-419c-98c6-75ded6fc9427",
      "name": "Fetch Lists",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Klaviyo Lists API \u2014 retrieves all list names in a single call. Converts raw IDs (e.g. RQVVZj) into human-readable names (e.g. VIP Customers) in the final report. Requires: Klaviyo API Key credential.",
      "position": [
        944,
        384
      ],
      "parameters": {
        "url": "https://a.klaviyo.com/api/lists/?fields%5Blist%5D=name",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "revision",
              "value": "2024-02-15"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "2811836f-536a-4cbb-b54a-3e4be669265d",
      "name": "Get Campaign Stats",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Single bulk POST to Klaviyo campaign-values-reports endpoint. Returns open_rate and click_rate per campaign for the last 90 days. conversion_metric_id X7ghUW is the Shopify Placed Order metric \u2014 required by Klaviyo's reporting API.",
      "position": [
        960,
        592
      ],
      "parameters": {
        "url": "https://a.klaviyo.com/api/campaign-values-reports/",
        "method": "POST",
        "options": {},
        "jsonBody": "{\n  \"data\": {\n    \"type\": \"campaign-values-report\",\n    \"attributes\": {\n      \"statistics\": [\n        \"open_rate\",\n        \"click_rate\"\n      ],\n      \"timeframe\": {\n        \"key\": \"last_90_days\"\n      },\n      \"conversion_metric_id\": \"X7ghUW\"\n    }\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "revision",
              "value": "2024-02-15"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "18ab8dca-f16d-4485-823e-3caa3024414a",
      "name": "Aggregate by Hour",
      "type": "n8n-nodes-base.code",
      "notes": "Core analysis node. Joins campaign metadata (from Flatten Campaigns) with stats (from Get Campaign Stats). Builds a list name lookup from Fetch Lists. Groups campaigns into (audience x send_hour) buckets, averages open rates and click rates. Flags slots with fewer than 3 campaigns as statistically thin.",
      "position": [
        1232,
        384
      ],
      "parameters": {
        "jsCode": "// Join campaign metadata with open-rate stats, aggregate into per-segment-per-hour buckets.\n\nconst MIN_CAMPAIGNS = 3;\n\nconst flattenedData = $('Flatten Campaigns').first().json;\nconst campaigns = flattenedData.campaigns || [];\n\nlet listsRaw = $('Fetch Lists').first().json;\nif (typeof listsRaw.data === 'string') {\n  try { listsRaw = JSON.parse(listsRaw.data); } catch(e) {}\n}\nconst listItems = Array.isArray(listsRaw) ? listsRaw : (Array.isArray(listsRaw?.data) ? listsRaw.data : []);\nconst nameMap = {};\nfor (const l of listItems) {\n  if (l.id && l.attributes?.name) nameMap[l.id] = l.attributes.name;\n}\n\nlet statsRaw = $input.first().json;\nif (typeof statsRaw.data === 'string') {\n  try { statsRaw = JSON.parse(statsRaw.data); } catch (e) {}\n}\nconst results = (statsRaw?.data?.attributes?.results || statsRaw?.attributes?.results || statsRaw?.results || []);\n\nconst statsMap = {};\nfor (const r of results) {\n  const campaignId = r.campaign_id || (r.groupings && r.groupings.campaign_id) || r.id;\n  if (!campaignId) continue;\n  const stats = r.statistics || {};\n  statsMap[campaignId] = {open_rate: parseFloat(stats.open_rate ?? 0), click_rate: parseFloat(stats.click_rate ?? 0)};\n}\n\nconst buckets = {};\nfor (const c of campaigns) {\n  const stats = statsMap[c.id];\n  if (!stats) continue;\n  const key = `${c.segment_key}__${c.send_hour}`;\n  if (!buckets[key]) {\n    buckets[key] = {segment_key: c.segment_key, send_hour: c.send_hour, label: `${String(c.send_hour).padStart(2, '0')}:00 UTC`, total_open_rate: 0, total_click_rate: 0, count: 0};\n  }\n  buckets[key].total_open_rate += stats.open_rate;\n  buckets[key].total_click_rate += stats.click_rate;\n  buckets[key].count += 1;\n}\n\nconst allBuckets = Object.values(buckets).map(b => ({...b, avg_open_rate: b.count > 0 ? b.total_open_rate / b.count : 0, avg_click_rate: b.count > 0 ? b.total_click_rate / b.count : 0}));\n\nconst qualified = allBuckets.filter(b => b.count >= MIN_CAMPAIGNS);\nconst warning = qualified.length === 0 ? `No time slots had ${MIN_CAMPAIGNS}+ campaigns yet. Showing all available data instead.` : null;\nconst outputBuckets = qualified.length > 0 ? qualified : allBuckets;\n\nreturn [{json: {buckets: outputBuckets, total_campaigns_analysed: campaigns.length, total_with_stats: results.length, warning, nameMap}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "11b6b623-ae13-4b8c-a2e4-d6c01dec8706",
      "name": "Format Email Report",
      "type": "n8n-nodes-base.code",
      "notes": "Builds the ranked HTML email. Sorts audience segments by best open rate first. Within each segment shows up to 5 time slots with medal icons for top 3. Resolves raw audience IDs to list names using the nameMap from Aggregate by Hour.",
      "position": [
        1472,
        384
      ],
      "parameters": {
        "jsCode": "const { buckets, total_campaigns_analysed, total_with_stats, warning, nameMap = {} } = $input.first().json;\n\nfunction resolveSegmentLabel(segId) {\n  if (!segId || segId === 'all') return 'All Subscribers';\n  return segId.split(',').map(id => nameMap[id] || id).join(' + ');\n}\n\nconst bySegment = {};\nfor (const b of buckets) {\n  const seg = b.segment_key || 'all';\n  if (!bySegment[seg]) bySegment[seg] = [];\n  bySegment[seg].push(b);\n}\n\nfor (const slots of Object.values(bySegment)) {\n  slots.sort((a, b) => b.avg_open_rate - a.avg_open_rate);\n}\n\nconst reportDate = new Date().toLocaleDateString('en-US', {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});\nconst medals = ['\ud83e\udd47', '\ud83e\udd48', '\ud83e\udd49'];\nconst rankColors = [{bg: '#fff8e1', border: '#f5a623'}, {bg: '#f5f5f5', border: '#9b9b9b'}, {bg: '#fdf6ee', border: '#c97c3a'}];\n\nconst segmentSections = Object.entries(bySegment)\n  .sort(([, slotsA], [, slotsB]) => (slotsB[0]?.avg_open_rate || 0) - (slotsA[0]?.avg_open_rate || 0))\n  .map(([segId, slots]) => {\n    const segLabel = resolveSegmentLabel(segId);\n    const rows = slots.slice(0, 5).map((slot, i) => {\n      const medal = medals[i] || `${i + 1}.`;\n      const openRateStr = (slot.avg_open_rate * 100).toFixed(1);\n      const clickRateStr = (slot.avg_click_rate * 100).toFixed(1);\n      const color = rankColors[i] || {bg: '#fafafa', border: '#ddd'};\n      return `<tr><td style=\"padding:0 0 6px 0;\"><div style=\"padding:12px 16px;background:${color.bg};border-left:4px solid ${color.border};border-radius:4px;\"><span style=\"font-size:16px;font-weight:bold;\">${medal} ${slot.label}</span><span style=\"color:#555;font-size:14px;\"> &mdash; <strong>${openRateStr}%</strong> open rate &nbsp;&bull;&nbsp; <strong>${clickRateStr}%</strong> click rate &nbsp;&bull;&nbsp; ${slot.count} campaign${slot.count !== 1 ? 's' : ''}</span></div></td></tr>`;\n    }).join('');\n    return `<h2 style=\"color:#333;border-bottom:2px solid #eee;padding-bottom:8px;margin-top:28px;\">${segLabel}</h2><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"border-collapse:collapse;\">${rows}</table>`;\n  }).join('');\n\nconst warningHtml = warning ? `<div style=\"background:#fff3cd;border:1px solid #ffc107;padding:12px 16px;border-radius:4px;margin-bottom:20px;font-size:14px;\">\u26a0\ufe0f ${warning}</div>` : '';\nconst noDataHtml = buckets.length === 0 ? '<p style=\"color:#666;\">No data available yet. Campaigns will appear here after they are sent.</p>' : '';\n\nconst html = `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body style=\"font-family:Arial,Helvetica,sans-serif;max-width:680px;margin:0 auto;padding:24px;color:#1a1a1a;\"><table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#4f46e5;border-radius:8px;margin-bottom:24px;\"><tr><td style=\"padding:20px 24px;\"><h1 style=\"margin:0;color:#fff;font-size:22px;\">\ud83d\udcca Campaign Send-Time Report</h1><p style=\"margin:4px 0 0;color:#c7d2fe;font-size:14px;\">Week of ${reportDate}</p></td></tr></table><p style=\"color:#555;font-size:14px;margin-bottom:6px;\"><strong>${total_campaigns_analysed}</strong> campaigns analysed &nbsp;&bull;&nbsp; <strong>${total_with_stats}</strong> with open-rate data &nbsp;&bull;&nbsp; Last 90 days &nbsp;&bull;&nbsp; Times shown in UTC</p>${warningHtml}${noDataHtml}${segmentSections}<hr style=\"border:none;border-top:1px solid #eee;margin:32px 0 16px;\"><p style=\"color:#999;font-size:12px;\">Generated automatically every Monday by your n8n Campaign Send-Time Optimizer.<br>Tip: if you see audience IDs instead of names, map them in the Aggregate by Hour node.</p></body></html>`;\nconst subject = `\ud83d\udcca Send-Time Report \u2014 ${reportDate}`;\nreturn [{ json: { subject, html, reportDate } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e8d75745-7a76-4502-8910-6d781e822fd6",
      "name": "Gmail \u2014 Send Report",
      "type": "n8n-nodes-base.gmail",
      "notes": "Delivers the weekly HTML send-time report to the configured inbox. Update the Send To address before activating the workflow.",
      "position": [
        1680,
        384
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.html }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "04756b4d-b1ce-41c5-afc5-cf7f8e95b975",
      "name": "Gmail \u2014 Error Alert",
      "type": "n8n-nodes-base.gmail",
      "notes": "Sends a failure notification email with the error message, the name of the failing node, and the execution start time. Update the Send To address before activating the workflow.",
      "position": [
        336,
        1040
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "=<p>The <strong>Campaign Send-Time Optimizer</strong> workflow failed.</p>\n<table style=\"font-family:Arial,sans-serif;font-size:14px;border-collapse:collapse;\">\n  <tr><td style=\"padding:4px 12px 4px 0;color:#555;\"><strong>Error:</strong></td><td>{{ $json.execution.error.message }}</td></tr>\n  <tr><td style=\"padding:4px 12px 4px 0;color:#555;\"><strong>Node:</strong></td><td>{{ $json.execution.error.node.name }}</td></tr>\n  <tr><td style=\"padding:4px 12px 4px 0;color:#555;\"><strong>Started:</strong></td><td>{{ $json.execution.startedAt }}</td></tr>\n</table>\n<p>Please check your n8n instance for the full execution log.</p>",
        "options": {
          "appendAttribution": false
        },
        "subject": "\u26a0\ufe0f Send-Time Optimizer Failed"
      },
      "typeVersion": 2.1
    },
    {
      "id": "afe02e01-3c08-4229-8d89-ff8585978601",
      "name": "Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "notes": "Catches any unhandled error thrown by any node in the main workflow chain. Passes error details \u2014 message, node name, execution time \u2014 to Gmail Error Alert.",
      "position": [
        112,
        1040
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "da68681d-b39b-45f6-a34a-1b6ede855bb8",
      "name": "Section \u2014 Error Handling Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        848
      ],
      "parameters": {
        "color": 7,
        "width": 992,
        "height": 448,
        "content": "### \ud83d\udedf Error Handling Branch\n\nCatches any unhandled failure from the main workflow chain and emails a structured alert with the error message, the failing node's name, and the execution start time \u2014 so you know within minutes that Monday morning's report didn't arrive."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "a9406359-1147-470b-b027-abc095607b97",
  "connections": {
    "Config": {
      "main": [
        [
          {
            "node": "Get Sent Campaigns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Lists": {
      "main": [
        [
          {
            "node": "Get Campaign Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate by Hour": {
      "main": [
        [
          {
            "node": "Format Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Campaigns": {
      "main": [
        [
          {
            "node": "Fetch Lists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Campaign Stats": {
      "main": [
        [
          {
            "node": "Aggregate by Hour",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Sent Campaigns": {
      "main": [
        [
          {
            "node": "Flatten Campaigns",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Email Report": {
      "main": [
        [
          {
            "node": "Gmail \u2014 Send Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule \u2014 Weekly Monday": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

How it works

Source: https://n8n.io/workflows/15552/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

This n8n workflow automates the entire lead generation pipeline from discovery to outreach: Location Grid Generation and Management Generates precise lat/lng grid points covering major US cities (New

Google Sheets, HTTP Request, SendGrid
Marketing & Ads

Workflow A — WhatsApp Lead Intake & Qualification. Uses postgres, httpRequest, errorTrigger. Scheduled trigger; 67 nodes.

Postgres, HTTP Request, Error Trigger
Marketing & Ads

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

Gmail, Google Sheets, Google Analytics +3
Marketing & Ads

This n8n workflow is a complete marketing automation system that connects to your CDP (Customer Data Platform), selects which flows to send, and delivers personalized emails using Brevo. It's modular

Noco Db, Sendinblue
Marketing & Ads

I created this workflow with great care to help you simplify your daily reporting routine. If you manage Meta Ads campaigns, you know how time-consuming it can be to open Ads Manager, filter data, and

Google Sheets, Facebook Graph Api, Gmail +1