AI驱动的Meta广告周度PDF报告 - 发送至Slack或邮箱
中级
这是一个Market Research, AI Summarization领域的自动化工作流,包含 14 个节点。主要使用 Set, Code, Slack, SplitOut, Pdforge 等节点。 使用GPT-4洞察生成Meta广告周度性能报告并通过Slack发送
前置要求
- •Slack Bot Token 或 Webhook URL
- •可能需要目标 API 的认证凭证
- •OpenAI API Key
使用的节点 (14)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "OGdHKG9jhEKDrZAC",
"meta": {
"instanceId": "33bfcf6494ef5d9b0fac5fde544f7913410659bb957613631505fa8056a7625a",
"templateCredsSetupCompleted": true
},
"name": "AI 驱动的 Meta 广告周度 PDF 报告 – 发送至您的 Slack 或邮箱",
"tags": [],
"nodes": [
{
"id": "e2a34995-dd70-432e-8bbc-3432421db01d",
"name": "拆分输出",
"type": "n8n-nodes-base.splitOut",
"position": [
-880,
40
],
"parameters": {
"options": {},
"fieldToSplitOut": "data"
},
"typeVersion": 1
},
{
"id": "a7c0b061-00a3-47a8-9ed6-3e8b840cc6a7",
"name": "HTTP 请求 - Meta 广告",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
-1100,
40
],
"parameters": {
"url": "=https://graph.facebook.com/v23.0/act_{{$json.ad_account_id}}/insights",
"options": {
"pagination": {
"pagination": {
"nextURL": "={{ $response.body.paging.next }}",
"paginationMode": "responseContainsNextURL",
"requestInterval": 500,
"completeExpression": "={{ !$response.body.paging.next }}",
"paginationCompleteWhen": "other"
}
}
},
"sendQuery": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"queryParameters": {
"parameters": [
{
"name": "fields",
"value": "=campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,reach,impressions,inline_link_clicks,inline_link_click_ctr,conversions,spend,cpc,cost_per_conversion,action_values,purchase_roas"
},
{
"name": "time_increment",
"value": "1"
},
{
"name": "date_preset",
"value": "={{ $json.date_range }}"
},
{
"name": "level",
"value": "ad"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"id": "Z9SGcPUjCFlHCdPM",
"name": "Meta Ads Auth"
}
},
"typeVersion": 4.2,
"alwaysOutputData": true
},
{
"id": "93076858-210e-4142-b7a1-1c0f5ba0d17d",
"name": "配置",
"type": "n8n-nodes-base.set",
"position": [
-1320,
40
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9941e108-1e6c-4050-8401-9e6125a847de",
"name": "ad_account_id",
"type": "string",
"value": "982086037409625232"
},
{
"id": "a9ca0cda-adc0-40fd-8f8c-8aa7437276c5",
"name": "date_range",
"type": "string",
"value": "last_7d"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "252d5491-0ecc-4c87-bce6-fa60e2aba035",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2040,
-80
],
"parameters": {
"width": 420,
"height": 520,
"content": "## 欢迎使用周度 Meta 广告报告工作流!"
},
"typeVersion": 1
},
{
"id": "fa9620e6-cee4-4d75-b19b-c6c25c56a555",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1380,
-80
],
"parameters": {
"color": 4,
"width": 220,
"height": 320,
"content": "**在此配置:**"
},
"typeVersion": 1
},
{
"id": "7ad3d2d7-571f-4e84-b8d3-a14a39b93a6e",
"name": "Pdforge",
"type": "n8n-nodes-pdforge.pdforge",
"position": [
100,
40
],
"parameters": {
"options": {},
"variables": "={{ JSON.stringify($json) }}",
"templateId": "meta_ads"
},
"credentials": {
"pdforgeApi": {
"id": "fFuvCIsBkIHHd0ct",
"name": "Pdforge account"
}
},
"typeVersion": 1
},
{
"id": "3ecfcea9-531e-47b2-8074-ed304b35f5e4",
"name": "格式化数据",
"type": "n8n-nodes-base.code",
"position": [
-660,
40
],
"parameters": {
"jsCode": "// 1. extract raw records\nconst records = $input.all().map(item => item.json);\n\n// 2. date utilities\nfunction parseYMD(s){ return new Date(s + 'T00:00:00'); }\nfunction formatYMD(d){\n const y = d.getFullYear();\n const m = String(d.getMonth()+1).padStart(2,'0');\n const da = String(d.getDate()).padStart(2,'0');\n return `${y}-${m}-${da}`;\n}\nfunction rangeDates(start, end){\n const a = [];\n let cur = new Date(start);\n while(cur <= end){\n a.push(formatYMD(cur));\n cur.setDate(cur.getDate()+1);\n }\n return a;\n}\n\n// 3. find global start/end\nconst dates = records.flatMap(r=>[r.date_start, r.date_stop]);\nconst minDate = new Date(Math.min(...dates.map(d=>parseYMD(d))));\nconst maxDate = new Date(Math.max(...dates.map(d=>parseYMD(d))));\nconst allLabels = rangeDates(minDate, maxDate);\n\n// 4. overview totals\nconst overviewAgg = records.reduce((acc, r) => {\n const imp = Number(r.impressions || 0);\n const clk = Number(r.inline_link_clicks || 0);\n const spd = Number(r.spend || 0);\n acc.impressions += imp;\n acc.clicks += clk;\n acc.investment += spd;\n return acc;\n}, { impressions:0, clicks:0, investment:0 });\n\nconst overview = {\n investment: { value: Number(overviewAgg.investment.toFixed(2)) },\n impressions: { value: overviewAgg.impressions },\n clicks: { value: overviewAgg.clicks },\n ctr: { \n value: overviewAgg.impressions\n ? `${(overviewAgg.clicks / overviewAgg.impressions * 100).toFixed(2)}%`\n : '0.00%' \n },\n cpc: { \n value: overviewAgg.clicks\n ? Number((overviewAgg.investment / overviewAgg.clicks).toFixed(4))\n : 0 \n }\n};\n\n// 5. day_by_day\nconst dayMap = records.reduce((m, r) => {\n const d = r.date_start;\n if (!m[d]) m[d] = { impressions:0, clicks:0 };\n m[d].impressions += Number(r.impressions || 0);\n m[d].clicks += Number(r.inline_link_clicks || 0);\n return m;\n}, {});\nconst day_by_day = {\n labels: allLabels,\n impressions: allLabels.map(d => dayMap[d]?.impressions || 0),\n clicks: allLabels.map(d => dayMap[d]?.clicks || 0),\n};\n\n// 6. helper to aggregate by key (com formatação de investimento em 2 casas)\nfunction aggBy(records, keyField){\n const map = {};\n records.forEach(r => {\n const key = r[keyField] || 'N/A';\n if (!map[key]) map[key] = { \n name: key, impressions:0, clicks:0, investment:0 \n };\n map[key].impressions += Number(r.impressions || 0);\n map[key].clicks += Number(r.inline_link_clicks || 0);\n map[key].investment += Number(r.spend || 0);\n });\n return Object.values(map).map(o => ({\n name: o.name,\n impressions: o.impressions,\n clicks: o.clicks,\n investment: Number(o.investment.toFixed(2)), // <-- 2 decimais aqui\n ctr: o.impressions\n ? `${(o.clicks / o.impressions * 100).toFixed(2)}%`\n : '0.00%',\n cpc: o.clicks\n ? Number((o.investment / o.clicks).toFixed(4))\n : 0,\n }));\n}\n\n// 7. campaigns, ad-sets, ads\nconst byCampaign = aggBy(records, \"campaign_name\");\nconst byAdSet = aggBy(records, \"adset_name\");\nconst byAd = aggBy(records, \"ad_name\");\n\n// 8. top 10 cheapest CPC\nconst top10Campaigns = [...byCampaign]\n .sort((a,b)=>a.cpc - b.cpc)\n .slice(0,10)\n .map(c=>({\n campaign_name: c.name,\n investment: c.investment,\n impressions: c.impressions,\n ctr: c.ctr,\n clicks: c.clicks,\n cpc: c.cpc\n }));\nconst top10AdSets = [...byAdSet]\n .sort((a,b)=>a.cpc - b.cpc)\n .slice(0,10)\n .map(a=>({\n ad_group_name: a.name,\n investment: a.investment,\n impressions: a.impressions,\n ctr: a.ctr,\n clicks: a.clicks,\n cpc: a.cpc\n }));\nconst top10Ads = [...byAd]\n .sort((a,b)=>a.cpc - b.cpc)\n .slice(0,10)\n .map(a=>({\n ad_group_name: a.name,\n investment: a.investment,\n impressions: a.impressions,\n ctr: a.ctr,\n clicks: a.clicks,\n cpc: a.cpc\n }));\n\n// 9. comparisons ordered by investment desc\nconst adsComparison = byAd.sort((a,b)=>b.investment - a.investment);\nconst adSetsComparison = byAdSet.sort((a,b)=>b.investment - a.investment);\n\n// 10. monta o relatório final\nconst report = {\n report_date: `${formatYMD(minDate)} - ${formatYMD(maxDate)}`,\n overview,\n day_by_day,\n top_10_campaigns: top10Campaigns,\n ad_set_comparison: {\n labels: adSetsComparison.map(a=>a.name),\n investment: adSetsComparison.map(a=>Number(a.investment.toFixed(2))),\n impressions: adSetsComparison.map(a=>a.impressions),\n clicks: adSetsComparison.map(a=>a.clicks),\n },\n top_10_ad_sets: top10AdSets,\n ads_comparison: {\n labels: adsComparison.map(a=>a.name),\n investment: adsComparison.map(a=>Number(a.investment.toFixed(2))),\n impressions: adsComparison.map(a=>a.impressions),\n clicks: adsComparison.map(a=>a.clicks),\n },\n top_10_ads: top10Ads\n};\n\n// 11. retorna um único item com o JSON pronto\nreturn [{ json: report }];"
},
"typeVersion": 2
},
{
"id": "4fc7c497-470f-4f24-adb2-e0b411c706f3",
"name": "下载二进制文件",
"type": "n8n-nodes-base.httpRequest",
"position": [
320,
40
],
"parameters": {
"url": "={{ $json.signedUrl }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "06bf787e-7304-401e-b522-4867bf3cec3d",
"name": "为 Meta 广告生成洞察",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-460,
40
],
"parameters": {
"text": "={{ JSON.stringify($json) }}",
"options": {
"systemMessage": "=## Persona\nYou are a data-driven Meta Ads Insights Agent: concise, analytical, and focused on turning raw campaign data into clear, actionable recommendations.\n\n## Objective\n- Ingest structured Meta Ads data (campaigns → adsets → ads). \n- Generate three tiers of performance insights: campaign, adset, ad. \n- Keep each insight 200–300 characters, in English, ending with a practical tip.\n\n## Rules\n### Formatting\n- Output **only** a JSON object with keys: `campaign_insights`, `adset_insights`, `ad_insights`. \n- Do **not** include any additional text or explanation. \n- Ensure each insight string is between **200 and 300 characters** (inclusive).\n\n### Content\n- All insights must be written in clear, idiomatic English. \n- Each insight must:\n 1. Summarize performance (e.g., spend vs. CTR/CPC comparisons). \n 2. Highlight one key strength or weakness. \n 3. Conclude with a single, concrete, actionable tip.\n\n### Constraints\n- Base insights strictly on the supplied data. \n- Do not reference hypothetical or external factors. \n- Do not break JSON syntax or embed markdown in the output.\n\n## Step by Step Instructions\n1. **Parse Input**: Read the incoming JSON payload of `campaigns` → `adsets` → `ads`. \n2. **Aggregate Metrics**: \n - For each campaign: compute total investment, average CTR (weighted by impressions), and average CPC. \n - For each adset: compute its CTR and CPC. \n - For each ad: read its individual metrics. \n3. **Identify Highlights**: \n - Campaign: find overall high or low performance signal. \n - Adset: select the best or worst by CTR or CPC. \n - Ad: select the top or bottom ad by CTR or CPC. \n4. **Compose Insights**: For each level, draft a 200–300 character string that: \n - Summarizes performance. \n - Notes one key strength or weakness. \n - Ends with “Tip: …” giving a clear improvement action. \n5. **Validate Length**: Confirm each insight is within 200–300 characters. \n6. **Return Output**: Emit exactly the JSON object described below.\n\n## Output Format\n```json\n{\n \"campaign_insights\": \"string (200–300 chars)\",\n \"adset_insights\": \"string (200–300 chars)\",\n \"ad_insights\": \"string (200–300 chars)\"\n}\n```"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2
},
{
"id": "be4f212e-78a9-4721-82e2-883e1ac02f9e",
"name": "OpenAI 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-560,
300
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "gpt-4.1"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "3hJLI2ahkB4Zjnwd",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "18aa7809-12f6-4ed4-9523-5b4a8efe8140",
"name": "结构化输出解析器",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-200,
260
],
"parameters": {
"jsonSchemaExample": "{\n \"campaign_insights\": \"string (200–300 chars)\",\n \"adset_insights\": \"string (200–300 chars)\",\n \"ad_insights\": \"string (200–300 chars)\"\n}"
},
"typeVersion": 1.3
},
{
"id": "49f0bd44-ad30-482e-9c05-9027081d7c7a",
"name": "生成变量",
"type": "n8n-nodes-base.code",
"position": [
-100,
40
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const base = $(\"Format data\").first().json\n\nreturn {\n ...base, \n insights_content: $json.output.campaign_insights,\n insights_ad_set_content:$json.output.adset_insights,\n insights_ads_content: $json.output.ad_insights,\n };\n"
},
"typeVersion": 2
},
{
"id": "d2bfb619-b65c-4371-a074-9384bdefd34b",
"name": "Slack - 发送带文件的消息",
"type": "n8n-nodes-base.slack",
"position": [
540,
40
],
"webhookId": "aae266d5-f681-4b86-bd2a-fceb1f5c182c",
"parameters": {
"options": {
"fileName": "=Meta ADS Report - {{ $('Generate variables').first().json.report_date }}",
"channelId": "C093723CAN6",
"initialComment": "📊 *Here's your Weekly Meta Ads Report* \\n"
},
"resource": "file",
"authentication": "oAuth2"
},
"credentials": {
"slackOAuth2Api": {
"id": "ombwJbyognfRtMoj",
"name": "Slack account"
}
},
"typeVersion": 2.3
},
{
"id": "0bd7647f-1f8b-4478-8e76-2a3f176ad39d",
"name": "每周一上午 8 点",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1560,
40
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
1
],
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1.2
}
],
"active": false,
"pinData": {},
"settings": {
"timezone": "America/Sao_Paulo",
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1",
"executionTimeout": -1
},
"versionId": "85fcbd4c-7f9b-4aae-b04e-99a0980bd474",
"connections": {
"Pdforge": {
"main": [
[
{
"node": "Download Binary",
"type": "main",
"index": 0
}
]
]
},
"Split Out": {
"main": [
[
{
"node": "Format data",
"type": "main",
"index": 0
}
]
]
},
"Format data": {
"main": [
[
{
"node": "Generating Insights for Meta Ads",
"type": "main",
"index": 0
}
]
]
},
"Configuration": {
"main": [
[
{
"node": "HTTP Request - Meta Ads",
"type": "main",
"index": 0
}
]
]
},
"Download Binary": {
"main": [
[
{
"node": "Slack - Send Message with File",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Generating Insights for Meta Ads",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Generate variables": {
"main": [
[
{
"node": "Pdforge",
"type": "main",
"index": 0
}
]
]
},
"Weekly at monday 8am": {
"main": [
[
{
"node": "Configuration",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request - Meta Ads": {
"main": [
[
{
"node": "Split Out",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Generating Insights for Meta Ads",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Slack - Send Message with File": {
"main": [
[]
]
},
"Generating Insights for Meta Ads": {
"main": [
[
{
"node": "Generate variables",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
中级 - 市场调研, AI 摘要总结
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
使用GPT-4洞察和Slack交付自动化每周SEO报告
通过GPT-4洞察和Slack交付自动化每周SEO报告
Set
Code
Merge
+8
42 节点Marcelo Abreu
市场调研
新抓取器_TechCrunch新闻-AI1
TechCrunch AI文章抓取与分类器,使用GPT-4.1-nano到Sheets和Telegram
Set
Code
Html
+12
18 节点Mujahid Kabae
市场调研
Twitter监控工作流
使用OpenAI、Google表格和Slack提醒自动化Twitter情感分析
If
Set
Slack
+10
15 节点InfyOm Technologies
市场调研
智能 RSS 源监控:AI 过滤、Baserow 存储与 Slack 提醒
具备 AI 过滤、Baserow 存储和 Slack 提醒的智能 RSS 源监控
Xml
Code
Slack
+9
28 节点Daniel Shashko
市场调研
AI内容侦察 - 每周竞品博客和新闻摘要
每周竞品内容摘要(Gemini和OpenAI、Google Sheets和Firecrawl)
Code
Gmail
Split Out
+9
22 节点Michael Yang
市场调研
基于Bright Data、OpenAI和Redis的高级多源AI研究
使用Bright Data、OpenAI和Redis进行高级多源AI研究
If
Set
Code
+15
43 节点Daniel Shashko
市场调研