This workflow corresponds to n8n.io template #17195 — we link there as the canonical source.
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 →
{
"id": "08ded00000000001",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Audit Meta pixels for duplicate conversions on your optimization events with claude",
"tags": [],
"nodes": [
{
"id": "overview",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1120,
32
],
"parameters": {
"width": 704,
"height": 784,
"content": "## Meta dedup & optimization-event auditor\n\nDouble-counting a PageView is noise. Double-counting the event your campaigns optimize toward wastes real ad spend. This workflow finds pixels where both the browser (Pixel) and server (CAPI) fire, and prioritizes the ones where an optimization event is at risk, so you verify dedup where it actually costs money.\n\n### How it works\n- Discovers the account's pixels and the events your ACTIVE campaigns optimize toward.\n- For each pixel, HTTP nodes read the browser-vs-server split and per-event counts from the Graph API.\n- Separates PageView from real conversions, flags each pixel (both sources active, browser-only, or server-only), and marks the events campaigns bid on.\n- Claude gives a next step per pixel, focused on the optimization events. It says verify, never claims confirmed double-counting.\n\n### Setup\n1. Edit the Set config node with your META_AD_ACCOUNT_ID. The token reads from your n8n env var META_ACCESS_TOKEN; also set ANTHROPIC_API_KEY.\n2. Run the manual trigger, or attach a Schedule for a weekly audit.\n\n### Customization\nTune ACTIVE_DAYS and MIN_EVENTS, or list specific DATASET_IDS. Swap stdout for Slack or email.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation & tracking. https://nocode.expert"
},
"typeVersion": 1
},
{
"id": "section-config-discover",
"name": "Section: Config & discover",
"type": "n8n-nodes-base.stickyNote",
"position": [
32,
192
],
"parameters": {
"color": 7,
"width": 1020,
"height": 508,
"content": "## 1. Config & discover\nSet the account, list its pixels, and pull the events your active campaigns optimize toward."
},
"typeVersion": 1
},
{
"id": "section-per-pixel-stats-http",
"name": "Section: Per-pixel stats (HTTP)",
"type": "n8n-nodes-base.stickyNote",
"position": [
1072,
192
],
"parameters": {
"color": 7,
"width": 784,
"height": 508,
"content": "## 2. Per-pixel stats (HTTP)\nHTTP nodes read each pixel's browser-vs-server split and per-event counts. Code only shapes and classifies."
},
"typeVersion": 1
},
{
"id": "section-explain-report",
"name": "Section: Explain & report",
"type": "n8n-nodes-base.stickyNote",
"position": [
1984,
176
],
"parameters": {
"color": 7,
"width": 736,
"height": 524,
"content": "## 3. Explain & report\nClaude gives a verify-dedup next step per pixel, focused on optimization events. Then print."
},
"typeVersion": 1
},
{
"id": "run-manually",
"name": "Run manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-208,
464
],
"parameters": {},
"typeVersion": 1
},
{
"id": "set-config-account-thresholds",
"name": "Set config (account & thresholds)",
"type": "n8n-nodes-base.set",
"position": [
128,
464
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "META_AD_ACCOUNT_ID",
"name": "META_AD_ACCOUNT_ID",
"type": "string",
"value": "act_YOUR_ACCOUNT_ID"
},
{
"id": "META_API_VERSION",
"name": "META_API_VERSION",
"type": "string",
"value": "v21.0"
},
{
"id": "META_ACCESS_TOKEN",
"name": "META_ACCESS_TOKEN",
"type": "string",
"value": "={{ $env.META_ACCESS_TOKEN }}"
},
{
"id": "DATASET_IDS",
"name": "DATASET_IDS",
"type": "string",
"value": ""
},
{
"id": "PIXEL_LIMIT",
"name": "PIXEL_LIMIT",
"type": "string",
"value": "50"
},
{
"id": "ACTIVE_DAYS",
"name": "ACTIVE_DAYS",
"type": "string",
"value": "14"
},
{
"id": "MIN_EVENTS",
"name": "MIN_EVENTS",
"type": "string",
"value": "50"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "list-account-pixels",
"name": "List account pixels",
"type": "n8n-nodes-base.httpRequest",
"position": [
352,
464
],
"parameters": {
"url": "=https://graph.facebook.com/{{ $('Set config (account & thresholds)').first().json.META_API_VERSION }}/{{ $('Set config (account & thresholds)').first().json.META_AD_ACCOUNT_ID }}/adspixels",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "fields",
"value": "id,name,last_fired_time"
},
{
"name": "limit",
"value": "100"
},
{
"name": "access_token",
"value": "={{ $('Set config (account & thresholds)').first().json.META_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "list-optimization-targets-adsets",
"name": "List optimization targets (adsets)",
"type": "n8n-nodes-base.httpRequest",
"position": [
528,
464
],
"parameters": {
"url": "=https://graph.facebook.com/{{ $('Set config (account & thresholds)').first().json.META_API_VERSION }}/{{ $('Set config (account & thresholds)').first().json.META_AD_ACCOUNT_ID }}/adsets",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "fields",
"value": "optimization_goal,effective_status,promoted_object{pixel_id,custom_event_type,custom_event_str,custom_conversion_id}"
},
{
"name": "limit",
"value": "500"
},
{
"name": "access_token",
"value": "={{ $('Set config (account & thresholds)').first().json.META_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "extract-optimization-targets",
"name": "Extract optimization targets",
"type": "n8n-nodes-base.code",
"position": [
720,
464
],
"parameters": {
"jsCode": "// Map each ACTIVE adset's optimization event to its pixel, so we can flag the events\n// campaigns actually bid on. Standard enums map to pixel event names; OTHER uses the\n// custom event string. (Custom conversions referenced only by id are not resolved here.)\nconst ENUM = { PURCHASE:'Purchase', LEAD:'Lead', INITIATED_CHECKOUT:'InitiateCheckout', COMPLETE_REGISTRATION:'CompleteRegistration', ADD_TO_CART:'AddToCart', CONTACT:'Contact', SUBSCRIBE:'Subscribe', ADD_PAYMENT_INFO:'AddPaymentInfo', SCHEDULE:'Schedule', START_TRIAL:'StartTrial', SUBMIT_APPLICATION:'SubmitApplication', VIEW_CONTENT:'ViewContent', SEARCH:'Search', DONATE:'Donate' };\nconst adsets = ($json.data) || [];\nconst optMap = {};\nfor (const a of adsets) {\n if (a.effective_status !== 'ACTIVE') continue;\n const po = a.promoted_object || {};\n if (!po.pixel_id) continue;\n const ev = po.custom_event_str || ENUM[po.custom_event_type] || (po.custom_event_type && po.custom_event_type !== 'OTHER' ? po.custom_event_type : null);\n if (!ev) continue;\n (optMap[po.pixel_id] = optMap[po.pixel_id] || []);\n if (!optMap[po.pixel_id].includes(ev)) optMap[po.pixel_id].push(ev);\n}\nreturn [{ json: { optMap } }];"
},
"typeVersion": 2
},
{
"id": "select-active-pixels",
"name": "Select active pixels",
"type": "n8n-nodes-base.code",
"position": [
912,
464
],
"parameters": {
"jsCode": "// Pick which pixels to audit. DEFAULT = only pixels your ACTIVE campaigns optimize on\n// (that is where dedup costs money, and it keeps Graph calls low to avoid rate limits).\n// Optimization-target pixels first, then fill with other recently-active pixels up to\n// PIXEL_LIMIT. DATASET_IDS overrides everything.\nconst cfg = $('Set config (account & thresholds)').first().json;\nconst ACTIVE_DAYS = Number(cfg.ACTIVE_DAYS) || 14;\nconst PIXEL_LIMIT = Number(cfg.PIXEL_LIMIT) || 15;\nconst DATASET_IDS = String(cfg.DATASET_IDS || '').split(',').map((s) => s.trim()).filter(Boolean);\nconst optMap = ($('Extract optimization targets').first().json.optMap) || {};\nconst list = ($('List account pixels').first().json.data) || [];\nconst byId = {}; list.forEach((p) => { byId[String(p.id)] = p.name; });\nconst now = Date.now();\nconst recent = list.filter((p) => p.last_fired_time && (now - new Date(p.last_fired_time).getTime()) < ACTIVE_DAYS * 864e5).map((p) => String(p.id));\nlet ids;\nif (DATASET_IDS.length) ids = DATASET_IDS; // explicit override\nelse {\n const optIds = Object.keys(optMap).filter((id) => byId[id]); // pixels campaigns bid on\n ids = optIds.length ? optIds : recent; // ONLY the money pixels if any exist\n}\nids = ids.slice(0, PIXEL_LIMIT);\nreturn ids.map((id) => ({ json: { pixel_id: id, name: byId[id] || id, is_opt_pixel: !!optMap[id] } }));"
},
"typeVersion": 2
},
{
"id": "get-browser-vs-server-split",
"name": "Get browser vs server split",
"type": "n8n-nodes-base.httpRequest",
"position": [
1152,
464
],
"parameters": {
"url": "=https://graph.facebook.com/{{ $('Set config (account & thresholds)').first().json.META_API_VERSION }}/{{ $json.pixel_id }}/stats",
"options": {
"batching": {
"batch": {
"batchSize": 1,
"batchInterval": 1200
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "aggregation",
"value": "event_source"
},
{
"name": "access_token",
"value": "={{ $('Set config (account & thresholds)').first().json.META_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "carry-pixel-id-source-split",
"name": "Carry pixel id + source split",
"type": "n8n-nodes-base.code",
"position": [
1360,
464
],
"parameters": {
"jsCode": "// Parse the browser-vs-server split and carry the pixel id/name forward.\nconst px = $('Select active pixels').all();\nreturn $input.all().map((it, i) => {\n const data = (it.json.data) || [];\n let server = 0, browser = 0;\n for (const w of data) for (const r of (w.data || [])) { const c = Number(r.count) || 0; if (r.value === 'SERVER') server += c; else if (r.value === 'BROWSER') browser += c; }\n return { json: { pixel_id: px[i].json.pixel_id, name: px[i].json.name, server, browser } };\n});"
},
"typeVersion": 2
},
{
"id": "get-per-event-counts",
"name": "Get per-event counts",
"type": "n8n-nodes-base.httpRequest",
"position": [
1536,
464
],
"parameters": {
"url": "=https://graph.facebook.com/{{ $('Set config (account & thresholds)').first().json.META_API_VERSION }}/{{ $json.pixel_id }}/stats",
"options": {
"batching": {
"batch": {
"batchSize": 1,
"batchInterval": 1200
}
}
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "aggregation",
"value": "event"
},
{
"name": "access_token",
"value": "={{ $('Set config (account & thresholds)').first().json.META_ACCESS_TOKEN }}"
}
]
}
},
"typeVersion": 4.2,
"continueOnFail": true
},
{
"id": "classify-dedup-risk-per-pixel",
"name": "Classify dedup risk per pixel",
"type": "n8n-nodes-base.code",
"position": [
1712,
464
],
"parameters": {
"jsCode": "// Combine source split (carried) + per-event counts (input) + optimization targets.\n// Flag pixels where an OPTIMIZATION event is dual-source - that is the costly case.\nconst cfg = $('Set config (account & thresholds)').first().json;\nconst MIN_EVENTS = Number(cfg.MIN_EVENTS) || 50;\nconst NON_CONV = new Set(['PageView', 'ViewContent', 'SubscribedButtonClick', 'Microdata']);\nconst optMap = ($('Extract optimization targets').first().json.optMap) || {};\nconst carried = $('Carry pixel id + source split').all().map((i) => i.json);\nconst evtResp = $input.all().map((i) => i.json);\nconst out = [];\nfor (let i = 0; i < carried.length; i++) {\n const { pixel_id, name, server, browser } = carried[i];\n const total = server + browser;\n if (total < MIN_EVENTS) continue;\n const server_pct = Math.round((100 * server) / total);\n const agg = {};\n for (const w of ((evtResp[i] || {}).data || [])) for (const r of (w.data || [])) agg[r.value] = (agg[r.value] || 0) + (Number(r.count) || 0);\n const optNames = optMap[pixel_id] || [];\n const events = Object.entries(agg).map(([n, c]) => ({ name: n, count: c, is_conversion: !NON_CONV.has(n), is_optimization: optNames.includes(n) }))\n .sort((a, b) => (b.is_optimization - a.is_optimization) || b.count - a.count);\n const conv_events = events.filter((e) => e.is_conversion).slice(0, 6);\n const opt_events = optNames;\n let status, risk;\n if (server_pct < 5) { status = 'browser_only'; risk = 'high'; }\n else if (server_pct > 95) { status = 'server_only'; risk = 'ok'; }\n else { status = 'dual_verify_dedup'; risk = 'verify'; }\n // Priority: an optimization event on a non-ok pixel is the one that costs money.\n const priority = (risk === 'ok' ? 0 : 10) + (opt_events.length ? 5 : 0) + Math.min(server_pct === 0 ? 0 : 4, 4);\n out.push({ pixel_id, name, server, browser, total, server_pct, pageview: agg['PageView'] || 0, opt_events, conv_events, status, risk, priority });\n}\nout.sort((a, b) => b.priority - a.priority || b.total - a.total);\nreturn out.map((o) => ({ json: o }));"
},
"typeVersion": 2
},
{
"id": "build-ai-dedup-summary-prompt",
"name": "Build AI dedup summary prompt",
"type": "n8n-nodes-base.code",
"position": [
2160,
464
],
"parameters": {
"jsCode": "// Ask Claude for a next step per flagged pixel, focused on the OPTIMIZATION events.\n// Never assert confirmed double-counting; never call PageView a conversion.\nconst pixels = $input.all().map((i) => i.json);\nconst flagged = pixels.filter((p) => p.risk === 'high' || p.risk === 'verify');\nconst system = 'You are a Meta server-side tracking specialist. Each pixel has browser vs server totals ACROSS ALL EVENTS (mostly PageView), its top conversion events, and opt_events = the events its ACTIVE campaigns optimize toward. dual_verify_dedup = both browser and server fire; IF they do not share an event_id, Meta double-counts. You CANNOT confirm double-counting from counts, so always say VERIFY. browser_only = CAPI effectively off. PRIORITISE the opt_events - a double-counted optimization event wastes ad spend. Never call PageView a conversion. For each pixel: one-line risk and one concrete next step naming the optimization event(s) to verify a shared event_id on (or add CAPI). Terse. Return ONLY JSON.';\nconst user = 'Pixels:\\n' + JSON.stringify(flagged) + '\\n\\nReturn JSON exactly: {\"summary\":\"one sentence, do NOT claim confirmed double-counting\",\"pixels\":[{\"name\":\"\",\"status\":\"\",\"action\":\"\"}]}';\nconst body = { model: 'claude-haiku-4-5', max_tokens: 1200, system, messages: [{ role: 'user', content: user }] };\nreturn [{ json: { body } }];"
},
"typeVersion": 2
},
{
"id": "explain-dedup-risks-with-claude-ai",
"name": "Explain dedup risks with Claude AI",
"type": "n8n-nodes-base.httpRequest",
"position": [
2384,
464
],
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"options": {},
"jsonBody": "={{ $json.body }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "x-api-key",
"value": "={{ $env.ANTHROPIC_API_KEY }}"
},
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "print-dedup-audit-actions",
"name": "Print dedup audit & actions",
"type": "n8n-nodes-base.code",
"position": [
2576,
464
],
"parameters": {
"jsCode": "// Print the audit: optimization events first, then source split + top conversions + verdict.\nconst ctx = $('Classify dedup risk per pixel').all().map((i) => i.json);\nconst raw = ($json.content && $json.content[0] && $json.content[0].text) || '{}';\nconst s = raw.indexOf('{'), e = raw.lastIndexOf('}');\nlet parsed = { summary: '', pixels: [] };\ntry { parsed = JSON.parse(raw.slice(s, e + 1)); } catch (err) {}\nconst actOf = {}; (parsed.pixels || []).forEach((p) => { actOf[p.name] = p; });\nconst label = { dual_verify_dedup: 'VERIFY DEDUP', browser_only: 'NO CAPI', server_only: 'ok' };\nconst out = [];\nout.push('');\nout.push('==========================================================');\nout.push(' META DEDUP / OPTIMIZATION-EVENT AUDIT | nocode.expert');\nout.push('==========================================================');\nout.push(parsed.summary || '');\nout.push('Pixels checked: ' + ctx.length);\nout.push('');\nfor (const p of ctx) {\n const flag = p.risk !== 'ok' ? ' <-- ' + label[p.status] : '';\n const optFlag = (p.opt_events.length && p.risk !== 'ok') ? ' [OPTIMIZED EVENT AT RISK]' : '';\n out.push('[' + String(p.server_pct).padStart(3) + '% server] ' + p.name + flag + optFlag);\n if (p.opt_events.length) out.push(' campaigns optimize toward: ' + p.opt_events.join(', '));\n out.push(' server ' + p.server + ' | browser ' + p.browser + ' (' + p.pageview + ' PageView of ' + p.total + ' total)');\n const conv = p.conv_events.map((c) => c.name + ' ' + c.count + (c.is_optimization ? '*' : '')).join(', ');\n if (conv) out.push(' conversion events: ' + conv);\n const a = actOf[p.name];\n if (a && a.action) out.push(' ACTION: ' + a.action);\n}\nout.push('==========================================================');\nout.push('* = event your active campaigns optimize toward');\nconsole.log(out.join('\\n'));\nreturn [{ json: { audit: ctx, summary: parsed.summary } }];"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "e25c8fd7-82af-4c4f-9c90-0c93bba60d7b",
"nodeGroups": [],
"connections": {
"Run manually": {
"main": [
[
{
"node": "Set config (account & thresholds)",
"type": "main",
"index": 0
}
]
]
},
"List account pixels": {
"main": [
[
{
"node": "List optimization targets (adsets)",
"type": "main",
"index": 0
}
]
]
},
"Get per-event counts": {
"main": [
[
{
"node": "Classify dedup risk per pixel",
"type": "main",
"index": 0
}
]
]
},
"Select active pixels": {
"main": [
[
{
"node": "Get browser vs server split",
"type": "main",
"index": 0
}
]
]
},
"Get browser vs server split": {
"main": [
[
{
"node": "Carry pixel id + source split",
"type": "main",
"index": 0
}
]
]
},
"Extract optimization targets": {
"main": [
[
{
"node": "Select active pixels",
"type": "main",
"index": 0
}
]
]
},
"Build AI dedup summary prompt": {
"main": [
[
{
"node": "Explain dedup risks with Claude AI",
"type": "main",
"index": 0
}
]
]
},
"Carry pixel id + source split": {
"main": [
[
{
"node": "Get per-event counts",
"type": "main",
"index": 0
}
]
]
},
"Classify dedup risk per pixel": {
"main": [
[
{
"node": "Build AI dedup summary prompt",
"type": "main",
"index": 0
}
]
]
},
"Set config (account & thresholds)": {
"main": [
[
{
"node": "List account pixels",
"type": "main",
"index": 0
}
]
]
},
"Explain dedup risks with Claude AI": {
"main": [
[
{
"node": "Print dedup audit & actions",
"type": "main",
"index": 0
}
]
]
},
"List optimization targets (adsets)": {
"main": [
[
{
"node": "Extract optimization targets",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow audits Meta (Facebook) Pixels for potential duplicate conversion counting by comparing browser Pixel vs Conversions API (server) activity and highlighting events active campaigns optimize for, then uses Anthropic Claude to generate pixel-by-pixel verification next…
Source: https://n8n.io/workflows/17195/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow listens for an “Approved” label on a Trello card, reads the AI draft bookkeeping JSON from card comments, and posts the corresponding transaction to Xero. It then adds a Xero deep link b
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
This workflow allows you to import any workflow from a file or another n8n instance and map the credentials easily. A multi-form setup guides you through the entire process At the beginning you have t
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.
[](https://youtu.be/c7yCZhmMjtI)