AutomationFlowsAI & RAG › Send Weekly Meta Ads Performance PDF Reports with Gemini and Gmail

Send Weekly Meta Ads Performance PDF Reports with Gemini and Gmail

BySPCTEK AI @spctek-ai on n8n.io

This workflow runs weekly to pull Meta Ads campaign and ad-level insights from the Meta Graph API, summarizes performance, generates charts with QuickChart, asks Google Gemini for written analysis, converts the resulting HTML report to a PDF via PDFShift, and emails it through…

Cron / scheduled trigger★★★★☆ complexityAI-powered22 nodesHTTP RequestGoogle GeminiGmail
AI & RAG Trigger: Cron / scheduled Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:
Send Weekly Meta Ads Performance PDF Reports with Gemini and Gmail — n8n workflow card showing HTTP Request, Google Gemini, Gmail integration

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

This workflow follows the Gmail → Googlegemini 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": "ITZjSHUzK8PdmrVy",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Meta Ads Weekly Performance Report",
  "tags": [
    {
      "id": "RQCopcc78zvJHcep",
      "name": "N8N Template Library",
      "createdAt": "2026-05-08T16:39:10.193Z",
      "updatedAt": "2026-05-08T16:39:10.193Z"
    }
  ],
  "nodes": [
    {
      "id": "b5d23f15-245c-404f-95c7-59f1d7eb47ea",
      "name": "Weekly Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -144,
        496
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "1d4c297b-66de-428c-80b4-8948afc3cc87",
      "name": "Config: Credentials & IDs",
      "type": "n8n-nodes-base.set",
      "position": [
        96,
        496
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "field-001",
              "name": "Meta_Ads_Account_ID",
              "type": "string",
              "value": "YOUR_AD_ACCOUNT_ID"
            },
            {
              "id": "field-002",
              "name": "Meta_Access_Token",
              "type": "string",
              "value": "YOUR_META_ACCESS_TOKEN"
            },
            {
              "id": "field-003",
              "name": "Report_Email",
              "type": "string",
              "value": "YOUR_REPORTING_EMAIL"
            },
            {
              "id": "field-004",
              "name": "Reporting_Schedule",
              "type": "string",
              "value": "Weekly"
            },
            {
              "id": "field-005",
              "name": "Company_Name",
              "type": "string",
              "value": "YOUR_COMPANY_NAME"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f2eb7b4d-e175-43a7-8ceb-505b10a9da41",
      "name": "Calculate Dates and Wrap Data",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        496
      ],
      "parameters": {
        "jsCode": "const config = $input.first().json;\n\nconst schedule = (config.Reporting_Schedule || 'Weekly').toLowerCase();\nlet daysBack = 7;\nlet datePreset = 'last_7d';\nif (schedule === 'biweekly') { daysBack = 14; datePreset = 'last_14d'; }\nelse if (schedule === 'monthly') { daysBack = 30; datePreset = 'last_30d'; }\n\nconst now = new Date();\nconst until = new Date(now);\nuntil.setDate(until.getDate() - 1);\nuntil.setHours(23, 59, 59, 999);\n\nconst since = new Date(until);\nsince.setDate(since.getDate() - (daysBack - 1));\nsince.setHours(0, 0, 0, 0);\n\nconst fmt = (d) => d.toISOString().split('T')[0];\nconst weekLabel = `${fmt(since)} to ${fmt(until)}`;\n\nreturn [{\n  json: {\n    since: fmt(since),\n    until: fmt(until),\n    date_preset: datePreset,\n    days_back: daysBack,\n    week_label: weekLabel,\n    data: {\n      Meta_Ads_Account_ID: config.Meta_Ads_Account_ID,\n      Meta_Access_Token: config.Meta_Access_Token,\n      Report_Email: config.Report_Email,\n      Reporting_Schedule: config.Reporting_Schedule,\n      Company_Name: config.Company_Name\n    }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9df9583d-f1cc-419e-ac0d-140ff0c425b7",
      "name": "Get Ad Account Info",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        896,
        720
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v23.0/act_{{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Ads_Account_ID }}",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "name,currency,timezone_name"
            },
            {
              "name": "access_token",
              "value": "={{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Access_Token }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "c2954a96-2d60-4518-b054-d2eb22d1fa42",
      "name": "Get Campaign Performance",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        752,
        336
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v23.0/act_{{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Ads_Account_ID }}/insights",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "level",
              "value": "campaign"
            },
            {
              "name": "fields",
              "value": "campaign_id,campaign_name,spend,impressions,reach,clicks,ctr,cpc,cpm,frequency,actions,action_values"
            },
            {
              "name": "time_increment",
              "value": "1"
            },
            {
              "name": "date_preset",
              "value": "={{ $node[\"Calculate Dates and Wrap Data\"].json.date_preset }}"
            },
            {
              "name": "limit",
              "value": "500"
            },
            {
              "name": "access_token",
              "value": "={{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Access_Token }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "68c52316-bed3-46bf-aa61-b2641c6c9f2c",
      "name": "Organize Campaign Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1056,
        336
      ],
      "parameters": {
        "jsCode": "const resp = $input.first().json;\nconst rows = resp.data || [];\n\nconst campaignsMap = {};\nconst dailyTotals = {};\n\nfunction getAction(actions, type) {\n  if (!Array.isArray(actions)) return 0;\n  const found = actions.find(a => a.action_type === type);\n  return found ? parseFloat(found.value) : 0;\n}\nfunction getActionValue(actionValues, type) {\n  if (!Array.isArray(actionValues)) return 0;\n  const found = actionValues.find(a => a.action_type === type);\n  return found ? parseFloat(found.value) : 0;\n}\n\nlet totalSpend = 0, totalImpressions = 0, totalClicks = 0, totalReach = 0, totalResults = 0, totalRevenue = 0;\n\nrows.forEach(row => {\n  const name = row.campaign_name || 'Unknown Campaign';\n  const spend = parseFloat(row.spend || 0);\n  const impressions = parseInt(row.impressions || 0);\n  const clicks = parseInt(row.clicks || 0);\n  const reach = parseInt(row.reach || 0);\n  const date = row.date_start;\n\n  const purchases = getAction(row.actions, 'purchase') || getAction(row.actions, 'omni_purchase');\n  const leads = getAction(row.actions, 'lead');\n  const landingPageViews = getAction(row.actions, 'landing_page_view');\n  const linkClicks = getAction(row.actions, 'link_click');\n  const results = purchases || leads || landingPageViews || linkClicks || 0;\n  const revenue = getActionValue(row.action_values, 'purchase') || getActionValue(row.action_values, 'omni_purchase') || 0;\n\n  if (!campaignsMap[name]) {\n    campaignsMap[name] = { campaign_id: row.campaign_id, campaign_name: name, spend: 0, impressions: 0, clicks: 0, reach: 0, results: 0, revenue: 0 };\n  }\n  const c = campaignsMap[name];\n  c.spend += spend;\n  c.impressions += impressions;\n  c.clicks += clicks;\n  c.reach = Math.max(c.reach, reach);\n  c.results += results;\n  c.revenue += revenue;\n\n  if (!dailyTotals[date]) dailyTotals[date] = { date, spend: 0, impressions: 0, clicks: 0 };\n  dailyTotals[date].spend += spend;\n  dailyTotals[date].impressions += impressions;\n  dailyTotals[date].clicks += clicks;\n\n  totalSpend += spend;\n  totalImpressions += impressions;\n  totalClicks += clicks;\n  totalReach += reach;\n  totalResults += results;\n  totalRevenue += revenue;\n});\n\nconst campaigns = Object.values(campaignsMap).map(c => ({\n  campaign_id: c.campaign_id,\n  campaign_name: c.campaign_name,\n  spend: Math.round(c.spend * 100) / 100,\n  impressions: c.impressions,\n  clicks: c.clicks,\n  reach: c.reach,\n  results: Math.round(c.results),\n  revenue: Math.round(c.revenue * 100) / 100,\n  ctr: c.impressions > 0 ? Math.round((c.clicks / c.impressions) * 10000) / 100 : 0,\n  cpc: c.clicks > 0 ? Math.round((c.spend / c.clicks) * 100) / 100 : 0,\n  cost_per_result: c.results > 0 ? Math.round((c.spend / c.results) * 100) / 100 : 0,\n  roas: (c.spend > 0 && c.revenue > 0) ? Math.round((c.revenue / c.spend) * 100) / 100 : null\n})).sort((a, b) => b.spend - a.spend);\n\nconst dailySeries = Object.values(dailyTotals).sort((a, b) => a.date.localeCompare(b.date));\n\nconst summary = {\n  total_spend: Math.round(totalSpend * 100) / 100,\n  total_impressions: totalImpressions,\n  total_clicks: totalClicks,\n  total_reach: totalReach,\n  total_results: Math.round(totalResults),\n  total_revenue: Math.round(totalRevenue * 100) / 100,\n  avg_ctr: totalImpressions > 0 ? Math.round((totalClicks / totalImpressions) * 10000) / 100 : 0,\n  avg_cpc: totalClicks > 0 ? Math.round((totalSpend / totalClicks) * 100) / 100 : 0,\n  avg_cpm: totalImpressions > 0 ? Math.round(((totalSpend / totalImpressions) * 1000) * 100) / 100 : 0,\n  avg_cost_per_result: totalResults > 0 ? Math.round((totalSpend / totalResults) * 100) / 100 : 0,\n  overall_roas: (totalSpend > 0 && totalRevenue > 0) ? Math.round((totalRevenue / totalSpend) * 100) / 100 : null,\n  campaign_count: campaigns.length\n};\n\nreturn [{ json: { data: { campaigns, dailySeries, summary } } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "885f951a-3184-4d87-beac-4dbe1973ef2d",
      "name": "Get Top Ads Performance",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        624,
        496
      ],
      "parameters": {
        "url": "=https://graph.facebook.com/v23.0/act_{{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Ads_Account_ID }}/insights",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "level",
              "value": "ad"
            },
            {
              "name": "fields",
              "value": "ad_id,ad_name,adset_name,campaign_name,spend,impressions,clicks,ctr,cpc,actions,action_values"
            },
            {
              "name": "date_preset",
              "value": "={{ $node[\"Calculate Dates and Wrap Data\"].json.date_preset }}"
            },
            {
              "name": "sort",
              "value": "spend_descending"
            },
            {
              "name": "limit",
              "value": "6"
            },
            {
              "name": "access_token",
              "value": "={{ $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Access_Token }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "0b4dcf27-cf32-48dd-80b3-50bb6cc7ae07",
      "name": "Format & Fetch Ad Creatives",
      "type": "n8n-nodes-base.code",
      "position": [
        896,
        496
      ],
      "parameters": {
        "jsCode": "const rows = $input.first().json.data || [];\nconst accessToken = $node[\"Calculate Dates and Wrap Data\"].json.data.Meta_Access_Token;\n\nfunction getAction(actions, type) {\n  if (!Array.isArray(actions)) return 0;\n  const found = actions.find(a => a.action_type === type);\n  return found ? parseFloat(found.value) : 0;\n}\n\nconst results = [];\n\nfor (const ad of rows) {\n  let creative = {};\n  try {\n    const resp = await this.helpers.request({\n      method: 'GET',\n      uri: `https://graph.facebook.com/v23.0/${ad.ad_id}`,\n      qs: {\n        fields: 'creative{thumbnail_url,image_url,body,title,object_type}',\n        access_token: accessToken\n      },\n      json: true\n    });\n    creative = resp.creative || {};\n  } catch (error) {\n    creative = { error: error.message };\n  }\n\n  const spend = parseFloat(ad.spend || 0);\n  const impressions = parseInt(ad.impressions || 0);\n  const clicks = parseInt(ad.clicks || 0);\n  const purchases = getAction(ad.actions, 'purchase') || getAction(ad.actions, 'omni_purchase');\n  const leads = getAction(ad.actions, 'lead');\n  const linkClicks = getAction(ad.actions, 'link_click');\n  const resultsCount = purchases || leads || linkClicks || 0;\n\n  results.push({\n    json: {\n      ad_id: ad.ad_id,\n      ad_name: ad.ad_name || 'Untitled Ad',\n      adset_name: ad.adset_name || '',\n      campaign_name: ad.campaign_name || '',\n      spend: Math.round(spend * 100) / 100,\n      impressions,\n      clicks,\n      ctr: parseFloat(ad.ctr || 0),\n      cpc: parseFloat(ad.cpc || 0),\n      results: Math.round(resultsCount),\n      cost_per_result: resultsCount > 0 ? Math.round((spend / resultsCount) * 100) / 100 : 0,\n      thumbnail_url: creative.thumbnail_url || creative.image_url || '',\n      body: creative.body || '',\n      title: creative.title || ''\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4d4cd8de-3c29-47ad-b602-ad35ec25e19a",
      "name": "Organize Ad Creative Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1200,
        496
      ],
      "parameters": {
        "jsCode": "const ads = $input.all().map(i => i.json);\n\nconst sorted = [...ads].sort((a, b) => b.spend - a.spend);\n\nconst bestCtr = ads.reduce((best, a) => (a.ctr > (best?.ctr || 0) ? a : best), null);\nconst bestSpend = sorted[0] || null;\n\nconst summary = {\n  ads_analyzed: ads.length,\n  best_ctr_ad: bestCtr ? bestCtr.ad_name : 'N/A',\n  best_ctr_value: bestCtr ? bestCtr.ctr : 0,\n  top_spend_ad: bestSpend ? bestSpend.ad_name : 'N/A',\n  top_spend_value: bestSpend ? bestSpend.spend : 0\n};\n\nreturn [{ json: { data: { ads: sorted, summary } } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "55ce7c45-8ba1-4fba-ba6e-1ad9242d186f",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1472,
        480
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "1f80d020-f4ec-441f-a7e0-8322e3d85e9e",
      "name": "Merge All Data & Build Charts",
      "type": "n8n-nodes-base.code",
      "position": [
        1712,
        496
      ],
      "parameters": {
        "jsCode": "const campaignData = $node[\"Organize Campaign Data\"].json.data;\nconst adData = $node[\"Organize Ad Creative Data\"].json.data;\nconst accountInfo = $node[\"Get Ad Account Info\"].json;\nconst weekLabel = $node[\"Calculate Dates and Wrap Data\"].json.week_label;\nconst since = $node[\"Calculate Dates and Wrap Data\"].json.since;\nconst until = $node[\"Calculate Dates and Wrap Data\"].json.until;\nconst companyName = $node[\"Calculate Dates and Wrap Data\"].json.data.Company_Name || accountInfo.name || 'Your Brand';\nconst currency = accountInfo.currency || 'USD';\n\nfunction chartUrl(config, width, height) {\n  let url = `https://quickchart.io/chart?c=${encodeURIComponent(JSON.stringify(config))}`;\n  if (width && height) url += `&width=${width}&height=${height}`;\n  return url;\n}\n\nfunction generateLineChart(labels, datasets, title) {\n  return {\n    type: \"line\",\n    data: { labels, datasets: datasets.map(d => ({ tension: 0.4, pointRadius: 3, fill: false, ...d })) },\n    options: { responsive: true, plugins: { title: { display: true, text: title, font: { size: 16 } } }, scales: { y: { beginAtZero: true } } }\n  };\n}\n\nfunction generateBarChart(labels, data, label, title, color) {\n  return {\n    type: \"bar\",\n    data: { labels, datasets: [{ label, data, backgroundColor: color }] },\n    options: { responsive: true, plugins: { title: { display: true, text: title, font: { size: 16 } }, legend: { display: false } }, scales: { y: { beginAtZero: true } } }\n  };\n}\n\nconst dates = campaignData.dailySeries.map(d => d.date.slice(5));\nconst spendData = campaignData.dailySeries.map(d => Math.round(d.spend * 100) / 100);\nconst impressionsData = campaignData.dailySeries.map(d => d.impressions);\nconst clicksData = campaignData.dailySeries.map(d => d.clicks);\nconst ctrData = campaignData.dailySeries.map(d => d.impressions > 0 ? Math.round((d.clicks / d.impressions) * 10000) / 100 : 0);\n\nconst spendChartUrl = chartUrl(generateLineChart(dates, [\n  { label: 'Spend', data: spendData, borderColor: '#e94560', backgroundColor: '#e9456022', fill: true }\n], 'Daily Ad Spend'), 700, 300);\n\nconst clicksImpressionsChartUrl = chartUrl(generateLineChart(dates, [\n  { label: 'Impressions', data: impressionsData, borderColor: '#1877F2' },\n  { label: 'Clicks', data: clicksData, borderColor: '#42B72A' }\n], 'Impressions vs Clicks'), 700, 300);\n\nconst topCampaigns = campaignData.campaigns.slice(0, 6);\nconst campaignSpendChartUrl = chartUrl(generateBarChart(\n  topCampaigns.map(c => c.campaign_name.slice(0, 18)),\n  topCampaigns.map(c => c.spend),\n  'Spend', 'Top Campaigns by Spend', '#0f3460'\n), 700, 320);\n\nconst ctrChartUrl = chartUrl(generateLineChart(dates, [\n  { label: 'CTR %', data: ctrData, borderColor: '#833AB4', backgroundColor: '#833AB422', fill: true }\n], 'Daily CTR Trend'), 700, 300);\n\nreturn [{ json: {\n  weekLabel, since, until, companyName, currency,\n  campaignData, adData,\n  charts: { spendChartUrl, clicksImpressionsChartUrl, campaignSpendChartUrl, ctrChartUrl }\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "184d5cd7-5ff0-44f6-8c0a-2a29ebe353f1",
      "name": "Prepare AI Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        1952,
        496
      ],
      "parameters": {
        "jsCode": "const merged = $input.first().json;\nconst c = merged.campaignData;\nconst a = merged.adData;\n\nconst systemPrompt = `You are a professional paid media analyst specializing in Meta Ads (Facebook & Instagram advertising). Analyze weekly campaign performance data. Always respond in English. You MUST use these EXACT section headers with ### prefix, nothing else before the content:\n\n### EXECUTIVE SUMMARY\n### CAMPAIGN PERFORMANCE ANALYSIS\n### CREATIVE PERFORMANCE ANALYSIS\n### BUDGET & EFFICIENCY INSIGHTS\n### NEXT STEPS\n\nUnder NEXT STEPS, write exactly 4 items each starting with \"\\u2022 \" on its own line. Do not add numbered prefixes, markdown bold, or extra formatting.`;\n\nconst userPrompt = `Analyze this Meta Ads performance data for the period ${merged.since} to ${merged.until} (currency: ${merged.currency}):\n\nACCOUNT SUMMARY:\n- Total Spend: ${c.summary.total_spend}\n- Total Impressions: ${c.summary.total_impressions}\n- Total Clicks: ${c.summary.total_clicks}\n- Total Reach: ${c.summary.total_reach}\n- Total Results: ${c.summary.total_results}\n- Average CTR: ${c.summary.avg_ctr}%\n- Average CPC: ${c.summary.avg_cpc}\n- Average CPM: ${c.summary.avg_cpm}\n- Average Cost Per Result: ${c.summary.avg_cost_per_result}\n- Overall ROAS: ${c.summary.overall_roas ?? 'N/A'}\n- Active Campaigns: ${c.summary.campaign_count}\n\nTOP CAMPAIGNS: ${JSON.stringify(c.campaigns.slice(0, 5).map(x => ({ name: x.campaign_name, spend: x.spend, ctr: x.ctr, cpc: x.cpc, results: x.results, cost_per_result: x.cost_per_result })))}\n\nTOP AD CREATIVES: ${JSON.stringify(a.ads.slice(0, 5).map(x => ({ name: x.ad_name, spend: x.spend, ctr: x.ctr, results: x.results, cost_per_result: x.cost_per_result })))}\n\nRespond now:`;\n\nreturn [{ json: { systemPrompt, userPrompt, merged } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "39574b7c-5f3c-4268-a902-d2aae7620f70",
      "name": "Message a model",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        2128,
        496
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-flash-preview",
          "cachedResultName": "models/gemini-3-flash-preview"
        },
        "options": {
          "maxOutputTokens": 1500
        },
        "messages": {
          "values": [
            {
              "role": "model",
              "content": "={{ $json.systemPrompt }}"
            },
            {
              "content": "={{ $json.userPrompt }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "2341749a-87d9-401b-a563-87d346f8c501",
      "name": "Build HTML Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2432,
        496
      ],
      "parameters": {
        "jsCode": "const geminiRaw = $node[\"Message a model\"].json;\n\nconst aiResponse =\n  (Array.isArray(geminiRaw) ? geminiRaw[0] : geminiRaw)\n    ?.content?.parts?.[0]?.text ||\n  geminiRaw?.candidates?.[0]?.content?.parts?.[0]?.text ||\n  geminiRaw?.message?.content ||\n  \"\";\n\nconst merged = $node[\"Merge All Data & Build Charts\"].json;\nconst c = merged.campaignData;\nconst a = merged.adData;\nconst charts = merged.charts;\nconst weekLabel = merged.weekLabel;\nconst companyName = merged.companyName;\nconst currency = merged.currency;\n\nfunction extractSection(text, heading) {\n  const escaped = heading.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n  const regex = new RegExp(`###\\\\s*${escaped}\\\\s*\\\\n([\\\\s\\\\S]*?)(?=###|$)`, 'i');\n  const match = text.match(regex);\n  return match ? match[1].trim() : '';\n}\n\nconst execSummary   = extractSection(aiResponse, 'EXECUTIVE SUMMARY')   || 'Analysis unavailable for this period.';\nconst campAnalysis  = extractSection(aiResponse, 'CAMPAIGN PERFORMANCE ANALYSIS') || 'No campaign analysis available.';\nconst creativeAnalysis = extractSection(aiResponse, 'CREATIVE PERFORMANCE ANALYSIS') || 'No creative analysis available.';\nconst budgetInsights = extractSection(aiResponse, 'BUDGET & EFFICIENCY INSIGHTS') || 'No budget insights available.';\nconst nextStepsRaw  = extractSection(aiResponse, 'NEXT STEPS') || '';\n\nconst stepLines = nextStepsRaw\n  .split('\\n')\n  .map(l => l.replace(/^[\\u2022\\-\\*]\\s*/, '').trim())\n  .filter(l => l.length > 10)\n  .slice(0, 4);\n\nwhile (stepLines.length < 4) stepLines.push('Review campaign performance and adjust budget allocation accordingly.');\n\nconst esc = (str) => String(str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst fmtMoney = (n) => `${currency} ${Number(n || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;\n\nconst campaignRowsHtml = c.campaigns.length === 0\n  ? '<tr><td colspan=\"7\" style=\"text-align:center;color:#6b7280;padding:16px;\">No campaign activity this period.</td></tr>'\n  : c.campaigns.map(camp => `\n    <tr>\n      <td>${esc(camp.campaign_name)}</td>\n      <td>${fmtMoney(camp.spend)}</td>\n      <td>${camp.impressions.toLocaleString()}</td>\n      <td>${camp.clicks.toLocaleString()}</td>\n      <td>${camp.ctr}%</td>\n      <td>${fmtMoney(camp.cpc)}</td>\n      <td>${camp.results.toLocaleString()}</td>\n    </tr>`).join('');\n\nconst creativeCardsHtml = a.ads.length === 0\n  ? '<p style=\"color:#6b7280;font-size:13px;\">No ad creative data available this period.</p>'\n  : a.ads.map(ad => `\n    <div class=\"creative-card\" style=\"break-inside:avoid;\">\n      ${ad.thumbnail_url ? `<div class=\"creative-thumb\" style=\"background-image:url('${esc(ad.thumbnail_url)}')\"></div>` : '<div class=\"creative-thumb creative-thumb-placeholder\">\\ud83c\\udfaf</div>'}\n      <div class=\"creative-body\">\n        <div class=\"creative-meta\">${esc(ad.campaign_name)}</div>\n        <p class=\"creative-text\">${esc((ad.ad_name || 'Untitled Ad').slice(0, 80))}</p>\n        <div class=\"creative-stats\">\n          <span class=\"cstat\">${fmtMoney(ad.spend)} spend</span>\n          <span class=\"cstat\">${ad.ctr}% CTR</span>\n          <span class=\"cstat\">${ad.results} results</span>\n        </div>\n      </div>\n    </div>`).join('');\n\nconst nextStepCardsHtml = stepLines.map((step, i) => `\n  <div class=\"step-card\" style=\"break-inside:avoid;\">\n    <div class=\"step-num\">${i + 1}</div>\n    <p class=\"step-text\">${esc(step)}</p>\n  </div>`).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Meta Ads Weekly Performance Report \\u2014 ${esc(weekLabel)}</title>\n<style>\n  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');\n  * { margin:0; padding:0; box-sizing:border-box; }\n  body { font-family:'Inter',sans-serif; background:#f0f2f5; color:#1a1a2e; -webkit-print-color-adjust:exact; print-color-adjust:exact; }\n  @page { margin: 28px 36px; }\n  .page { max-width:900px; margin:0 auto; background:#fff; padding:0 0 24px 0; }\n  .section { padding:32px 40px; border-bottom:1px solid #f0f2f5; break-inside:avoid; }\n  .section-title { font-size:11px; font-weight:700; letter-spacing:3px; text-transform:uppercase; color:#e94560; margin-bottom:20px; }\n  .section h2 { font-size:22px; font-weight:700; margin-bottom:16px; color:#1a1a2e; }\n\n  .header { background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%); color:#fff; padding:40px; break-inside:avoid; }\n  .header-top { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; }\n  .brand { font-size:13px; font-weight:600; letter-spacing:3px; text-transform:uppercase; color:#e94560; }\n  .report-badge { background:rgba(233,69,96,0.2); border:1px solid #e94560; color:#e94560; padding:4px 14px; border-radius:20px; font-size:12px; font-weight:600; }\n  .header h1 { font-size:32px; font-weight:800; margin-bottom:8px; }\n  .header .week { font-size:15px; color:rgba(255,255,255,0.6); }\n  .header-stats { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-top:28px; }\n  .hstat { background:rgba(255,255,255,0.07); border-radius:12px; padding:16px; text-align:center; break-inside:avoid; }\n  .hstat-val { font-size:22px; font-weight:800; color:#e94560; }\n  .hstat-label { font-size:11px; color:rgba(255,255,255,0.5); text-transform:uppercase; letter-spacing:1px; margin-top:4px; }\n\n  .combined-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:8px; }\n  .combined-card { background:linear-gradient(135deg,#1a1a2e,#0f3460); border-radius:12px; padding:20px; text-align:center; break-inside:avoid; }\n  .combined-val { font-size:24px; font-weight:800; color:#e94560; }\n  .combined-label { font-size:11px; color:rgba(255,255,255,0.6); text-transform:uppercase; letter-spacing:1px; margin-top:6px; }\n  .combined-sub { font-size:10px; color:rgba(255,255,255,0.35); margin-top:2px; }\n\n  .stats-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:24px; }\n  .stat-card { background:#f8f9ff; border-radius:12px; padding:16px; text-align:center; border:1px solid #e8eaf6; break-inside:avoid; }\n  .stat-val { font-size:22px; font-weight:800; color:#1a1a2e; }\n  .stat-label { font-size:11px; color:#6b7280; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px; }\n\n  .chart-row { display:grid; grid-template-columns:1fr 1fr; gap:20px; margin:20px 0; break-inside:avoid; }\n  .chart-box { background:#f8f9ff; border-radius:12px; padding:16px; border:1px solid #e8eaf6; break-inside:avoid; }\n  .chart-box img { width:100%; border-radius:8px; display:block; }\n  .chart-title { font-size:12px; font-weight:600; color:#6b7280; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; }\n\n  .ai-section { background:linear-gradient(135deg,#f0f4ff,#f5f0ff); border-radius:16px; padding:28px; margin-bottom:24px; border:1px solid #e0e7ff; break-inside:avoid; }\n  .ai-badge { display:inline-flex; align-items:center; gap:6px; background:#e94560; color:#fff; padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700; letter-spacing:1px; text-transform:uppercase; margin-bottom:16px; }\n  .ai-text { font-size:14px; line-height:1.8; color:#374151; white-space:pre-wrap; }\n\n  .campaign-table { width:100%; border-collapse:collapse; margin-top:16px; font-size:13px; }\n  .campaign-table th { background:#1a1a2e; color:#fff; text-align:left; padding:10px 12px; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }\n  .campaign-table td { padding:10px 12px; border-bottom:1px solid #eef0f5; color:#374151; }\n  .campaign-table tr:nth-child(even) { background:#f8f9ff; }\n\n  .creative-grid { display:flex; flex-wrap:wrap; gap:16px; margin-top:20px; }\n  .creative-card { flex:1 1 calc(33% - 11px); min-width:200px; background:#fff; border:1px solid #e8eaf6; border-radius:12px; overflow:hidden; break-inside:avoid; }\n  .creative-thumb { width:100%; height:130px; background-size:cover; background-position:center; background-color:#f0f2f5; }\n  .creative-thumb-placeholder { display:flex; align-items:center; justify-content:center; font-size:32px; }\n  .creative-body { padding:12px; }\n  .creative-meta { font-size:10px; color:#9ca3af; margin-bottom:4px; text-transform:uppercase; letter-spacing:0.5px; }\n  .creative-text { font-size:12px; line-height:1.5; color:#374151; margin-bottom:8px; min-height:32px; }\n  .creative-stats { display:flex; flex-wrap:wrap; gap:6px; }\n  .cstat { font-size:10px; color:#6b7280; background:#f3f4f6; padding:3px 7px; border-radius:6px; }\n\n  .next-steps { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:16px; }\n  .step-card { background:linear-gradient(135deg,#fff5f7,#fff); border:1px solid #fecdd3; border-radius:12px; padding:20px; break-inside:avoid; }\n  .step-num { width:28px; height:28px; background:#e94560; color:#fff; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; margin-bottom:10px; }\n  .step-text { font-size:13px; line-height:1.6; color:#374151; }\n\n  .footer { background:#1a1a2e; color:rgba(255,255,255,0.4); text-align:center; padding:24px; font-size:12px; break-inside:avoid; }\n  .footer strong { color:#e94560; }\n</style>\n</head>\n<body>\n<div class=\"page\">\n\n  <div class=\"header\">\n    <div class=\"header-top\">\n      <span class=\"brand\">${esc(companyName)}</span>\n      <span class=\"report-badge\">Meta Ads Weekly Report</span>\n    </div>\n    <h1>Ads Performance Report</h1>\n    <p class=\"week\">Period: ${esc(weekLabel)}</p>\n    <div class=\"header-stats\">\n      <div class=\"hstat\"><div class=\"hstat-val\">${fmtMoney(c.summary.total_spend)}</div><div class=\"hstat-label\">Total Spend</div></div>\n      <div class=\"hstat\"><div class=\"hstat-val\">${c.summary.total_clicks.toLocaleString()}</div><div class=\"hstat-label\">Total Clicks</div></div>\n      <div class=\"hstat\"><div class=\"hstat-val\">${c.summary.avg_ctr}%</div><div class=\"hstat-label\">Average CTR</div></div>\n      <div class=\"hstat\"><div class=\"hstat-val\">${c.summary.total_results.toLocaleString()}</div><div class=\"hstat-label\">Total Results</div></div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Account Performance Overview</div>\n    <div class=\"combined-grid\">\n      <div class=\"combined-card\"><div class=\"combined-val\">${fmtMoney(c.summary.avg_cpc)}</div><div class=\"combined-label\">Avg CPC</div><div class=\"combined-sub\">Cost per click</div></div>\n      <div class=\"combined-card\"><div class=\"combined-val\">${fmtMoney(c.summary.avg_cpm)}</div><div class=\"combined-label\">Avg CPM</div><div class=\"combined-sub\">Cost per 1,000 impr.</div></div>\n      <div class=\"combined-card\"><div class=\"combined-val\">${fmtMoney(c.summary.avg_cost_per_result)}</div><div class=\"combined-label\">Cost / Result</div><div class=\"combined-sub\">Blended</div></div>\n      <div class=\"combined-card\"><div class=\"combined-val\">${c.summary.overall_roas ?? 'N/A'}</div><div class=\"combined-label\">ROAS</div><div class=\"combined-sub\">Revenue / spend</div></div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">AI-Powered Analysis</div>\n    <div class=\"ai-section\">\n      <span class=\"ai-badge\">\\ud83e\\udd16 Executive Summary</span>\n      <p class=\"ai-text\">${esc(execSummary)}</p>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Campaign Performance</div>\n    <div class=\"stats-grid\">\n      <div class=\"stat-card\"><div class=\"stat-val\">${c.summary.campaign_count}</div><div class=\"stat-label\">Active Campaigns</div></div>\n      <div class=\"stat-card\"><div class=\"stat-val\">${c.summary.total_impressions.toLocaleString()}</div><div class=\"stat-label\">Impressions</div></div>\n      <div class=\"stat-card\"><div class=\"stat-val\">${c.summary.total_reach.toLocaleString()}</div><div class=\"stat-label\">Reach</div></div>\n      <div class=\"stat-card\"><div class=\"stat-val\">${fmtMoney(c.summary.total_revenue)}</div><div class=\"stat-label\">Revenue Tracked</div></div>\n    </div>\n    <div class=\"chart-row\">\n      <div class=\"chart-box\"><div class=\"chart-title\">Daily Ad Spend</div><img src=\"${charts.spendChartUrl}\" alt=\"Daily Spend\"/></div>\n      <div class=\"chart-box\"><div class=\"chart-title\">Impressions vs Clicks</div><img src=\"${charts.clicksImpressionsChartUrl}\" alt=\"Impressions vs Clicks\"/></div>\n    </div>\n    <div class=\"chart-row\">\n      <div class=\"chart-box\"><div class=\"chart-title\">Top Campaigns by Spend</div><img src=\"${charts.campaignSpendChartUrl}\" alt=\"Top Campaigns\"/></div>\n      <div class=\"chart-box\"><div class=\"chart-title\">Daily CTR Trend</div><img src=\"${charts.ctrChartUrl}\" alt=\"CTR Trend\"/></div>\n    </div>\n    <div class=\"ai-section\">\n      <span class=\"ai-badge\">\\ud83d\\udcca Campaign Insights</span>\n      <p class=\"ai-text\">${esc(campAnalysis)}</p>\n    </div>\n    <div class=\"section-title\" style=\"margin-top:8px;\">Campaign Breakdown</div>\n    <table class=\"campaign-table\">\n      <thead><tr><th>Campaign</th><th>Spend</th><th>Impressions</th><th>Clicks</th><th>CTR</th><th>CPC</th><th>Results</th></tr></thead>\n      <tbody>${campaignRowsHtml}</tbody>\n    </table>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Creative Performance</div>\n    <div class=\"ai-section\">\n      <span class=\"ai-badge\">\\ud83c\\udfa8 Creative Insights</span>\n      <p class=\"ai-text\">${esc(creativeAnalysis)}</p>\n    </div>\n    <div class=\"section-title\" style=\"margin-top:8px;\">Top Performing Ads This Period</div>\n    <div class=\"creative-grid\">${creativeCardsHtml}</div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Budget &amp; Efficiency</div>\n    <div class=\"ai-section\">\n      <span class=\"ai-badge\">\\ud83d\\udcb0 Budget Insights</span>\n      <p class=\"ai-text\">${esc(budgetInsights)}</p>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Action Plan</div>\n    <h2>Next Steps</h2>\n    <div class=\"next-steps\">${nextStepCardsHtml}</div>\n  </div>\n\n  <div class=\"footer\">\n    Generated automatically by <strong>Meta Ads Weekly Performance Workflow</strong> &bull; ${esc(weekLabel)}\n  </div>\n\n</div>\n</body>\n</html>`;\n\nreturn [{ json: { html, weekLabel } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c2722eb0-6262-4c09-8fba-fbb5843f18eb",
      "name": "Convert HTML to PDF",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Uses PDFShift (api.pdfshift.io) - free tier available. Alternative services: html2pdf.app, Gotenberg, Browserless.",
      "position": [
        2672,
        496
      ],
      "parameters": {
        "url": "https://api.pdfshift.io/v3/convert/pdf",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "output.pdf"
            }
          }
        },
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "source",
              "value": "={{ $json.html }}"
            },
            {
              "name": "landscape",
              "value": "false"
            },
            {
              "name": "use_print",
              "value": "false"
            }
          ]
        },
        "genericAuthType": "httpBasicAuth",
        "headerParameters": {
          "parameters": []
        }
      },
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2
    },
    {
      "id": "16a3f922-da43-4580-8702-4ec02d946548",
      "name": "Send Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2912,
        496
      ],
      "parameters": {
        "sendTo": "={{ $node[\"Calculate Dates and Wrap Data\"].json.data.Report_Email }}",
        "message": "Here's your Meta Ads weekly performance report covering campaign and ad creative performance.",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "output.pdf"
              }
            ]
          }
        },
        "subject": "=\ud83d\udcca Meta Ads Weekly Performance Report \u2014 {{ $node[\"Calculate Dates and Wrap Data\"].json.week_label }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9f1f7bf7-a906-497b-9605-7f1116726938",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 576,
        "height": 408,
        "content": "# \ud83d\udcca Meta Ads Weekly Performance Report\n\nRuns every Monday at 8AM (or on your chosen schedule).\nPulls Meta Ads campaign + ad creative data \u2192 builds charts \u2192 AI analysis via Gemini \u2192 HTML \u2192 PDF \u2192 Gmail.\n\n**Output:** Branded PDF report delivered to your inbox automatically, covering spend, CTR, CPC, ROAS, campaign breakdown and top-performing ad creatives."
      },
      "typeVersion": 1
    },
    {
      "id": "08b46bc7-664b-43ae-86e9-50b9f85d98d7",
      "name": "Sticky Note - Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -176,
        64
      ],
      "parameters": {
        "width": 672,
        "height": 404,
        "content": "### \u2699\ufe0f STEP 1 \u2014 Fill Your Credentials Here\n\nEdit this Set node with your actual values:\n\n- *Meta_Ads_Account_ID* \u2192 Your Ad Account ID (numeric only, without the \"act_\" prefix)\n- *Meta_Access_Token* \u2192 Long-lived System User or User Access Token with `ads_read` permission\n- *Report_Email* \u2192 Email address to receive the PDF report\n- *Reporting_Schedule* \u2192 \"Weekly\", \"Biweekly\", or \"Monthly\" \u2014 controls the date range pulled from Meta\n- *Company_Name* \u2192 Shown on the report header (optional, falls back to your ad account name)\n\n\u26a0\ufe0f Never commit real tokens to public templates.\nUse n8n Credentials or environment variables for production."
      },
      "typeVersion": 1
    },
    {
      "id": "98a3a3ae-9738-400c-9e34-408d9299deef",
      "name": "Sticky Note - Campaign Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 336,
        "content": "### \ud83d\udcc8 Campaign Performance Pipeline\n\n1. *Get Ad Account Info* \u2014 fetches account name + currency for report branding\n2. *Get Campaign Performance* \u2014 daily campaign-level insights (spend, impressions, clicks, reach, actions)\n3. *Organize Campaign Data* \u2014 aggregates into campaigns[], dailySeries[], summary{} (spend, CTR, CPC, CPM, cost/result, ROAS)\n\nUses Graph API v23.0, `level=campaign`, `time_increment=1`.\n*Date range is driven by the Reporting_Schedule field (last 7 / 14 / 30 days).*"
      },
      "typeVersion": 1
    },
    {
      "id": "fbb10a76-c9db-42b1-aaca-3bd40427d755",
      "name": "Sticky Note - Creative Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 800,
        "height": 320,
        "content": "### \ud83c\udfa8 Ad Creative Performance Pipeline\n\n1. *Get Top Ads Performance* \u2014 top 6 ads by spend (`level=ad`, sorted `spend_descending`)\n2. *Format & Fetch Ad Creatives* \u2014 loops each ad and calls `/{ad_id}?fields=creative{thumbnail_url,image_url,body,title}` to pull the creative thumbnail + copy\n3. *Organize Ad Creative Data* \u2014 merges performance + creative into ads[] with CTR, cost/result, and identifies best performers\n\n*\u26a0\ufe0f One extra API call per ad (max 6) \u2014 keep the limit reasonable to stay within Meta's rate limits.*"
      },
      "typeVersion": 1
    },
    {
      "id": "d22d9ebf-1fab-475e-8e2c-0bc860f4265c",
      "name": "Sticky Note - AI Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1696,
        -32
      ],
      "parameters": {
        "width": 768,
        "height": 480,
        "content": "### \ud83e\udd16 AI Analysis \u2014 Google Gemini\n\n*Model: gemini-3-flash-preview*\n*Max tokens: 1500*\n\nThe prompt instructs Gemini to return exactly 5 sections:\n_*### EXECUTIVE SUMMARY*_\n_*### CAMPAIGN PERFORMANCE ANALYSIS*_\n_*### CREATIVE PERFORMANCE ANALYSIS*_\n_*### BUDGET & EFFICIENCY INSIGHTS*_\n_*### NEXT STEPS (4 bullet points)*_\n\nThe *Build HTML Report* node parses these sections using ### markers.\nIf a section is missing, fallback text is used automatically.\n\nTo switch to OpenAI/Claude: replace the *Message a model* node and update\nthe `aiResponse` extraction logic in *Build HTML Report* accordingly."
      },
      "typeVersion": 1
    },
    {
      "id": "a87da056-a80b-4a38-bde3-47dc422819a5",
      "name": "Sticky Note - PDF & Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2512,
        -32
      ],
      "parameters": {
        "width": 592,
        "height": 456,
        "content": "### \ud83d\udcc4 PDF Conversion + Email Delivery\n\n*PDF Service: PDFShift (api.pdfshift.io/v3/convert/pdf)*\n- Add your PDFShift API key under HTTP Basic Auth credentials\n- Free tier = 50 PDF conversions/month\n- Portrait mode, screen CSS rendering\n\nAlternative services: html2pdf.app, Gotenberg, Browserless\n\n### Email: Sent via Gmail OAuth2\n- Attach: output.pdf (binary from PDFShift)\n- Subject auto-includes the reporting period\n- Recipient pulled from Config node (Report_Email)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "9d8a1bf4-9d24-4f1d-a495-c1b79c73921c",
  "nodeGroups": [],
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Merge All Data & Build Charts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Trigger": {
      "main": [
        [
          {
            "node": "Config: Credentials & IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Build HTML Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build HTML Report": {
      "main": [
        [
          {
            "node": "Convert HTML to PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare AI Prompt": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert HTML to PDF": {
      "main": [
        [
          {
            "node": "Send Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Ad Account Info": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Organize Campaign Data": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Top Ads Performance": {
      "main": [
        [
          {
            "node": "Format & Fetch Ad Creatives",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Campaign Performance": {
      "main": [
        [
          {
            "node": "Organize Campaign Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config: Credentials & IDs": {
      "main": [
        [
          {
            "node": "Calculate Dates and Wrap Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Organize Ad Creative Data": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Format & Fetch Ad Creatives": {
      "main": [
        [
          {
            "node": "Organize Ad Creative Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Dates and Wrap Data": {
      "main": [
        [
          {
            "node": "Get Ad Account Info",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Campaign Performance",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Top Ads Performance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Data & Build Charts": {
      "main": [
        [
          {
            "node": "Prepare AI Prompt",
            "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.

Pro

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

About this workflow

This workflow runs weekly to pull Meta Ads campaign and ad-level insights from the Meta Graph API, summarizes performance, generates charts with QuickChart, asks Google Gemini for written analysis, converts the resulting HTML report to a PDF via PDFShift, and emails it through…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow is a complete outbound automation system that discovers local businesses, extracts contact emails, generates personalized cold emails using AI, and runs a multi-step follow-up sequence —

Stop And Error, Google Sheets, HTTP Request +2
AI & RAG

This workflow runs every minute to pull accounts from Salesforce, enrich each customer with order, payment, support, and marketing data via HTTP APIs, analyze the combined profile with Google Gemini,

HTTP Request, Salesforce, Gmail +2
AI & RAG

N8Nflow Zhtw. Uses executeCommand, readBinaryFiles, httpRequest, googleGemini. Scheduled trigger; 28 nodes.

Execute Command, Read Binary Files, HTTP Request +2
AI & RAG

N8Nflow En. Uses executeCommand, readBinaryFiles, httpRequest, googleGemini. Scheduled trigger; 28 nodes.

Execute Command, Read Binary Files, HTTP Request +2
AI & RAG

This workflow automatically fetches the latest business news, analyzes its impact on clients using AI and sends alerts for high-impact articles while logging all processed data in Google Sheets. It en

HTTP Request, Google Gemini, Gmail +1