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