This workflow corresponds to n8n.io template #17011 — we link there as the canonical source.
This workflow follows the Facebookgraphapi → HTTP Request 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 →
{
"id": "mvxVBIvdjGEVWgGX",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Monitor Meta CAPI event match quality (EMQ) across every pixel",
"tags": [],
"nodes": [
{
"id": "cb460661-be5e-4bd7-8907-494c52e13fba",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
10304,
784
],
"parameters": {
"width": 604,
"height": 536,
"content": "## Meta CAPI Event Match Quality (EMQ) Monitor\n\nWatches your Meta pixels for match-quality decay. Every CAPI template only SENDS events; this one MONITORS how well they match, so you catch a silent signal-quality drop before it wrecks optimization and costs.\n\n### How it works\n- Discovers the ad account's pixels and keeps the recently-active ones.\n- Pulls each pixel's server-event match stats (PII coverage per event) from the Graph API.\n- Scores match quality 0-100, weighting the conversion events you optimize on three times heavier.\n- Flags any pixel below your alert threshold and has Claude explain the gap and the exact fix (hashed email/phone, fbc/fbp, Advanced Matching).\n\n### Setup\n1. Add to your .env: META_ACCESS_TOKEN, META_AD_ACCOUNT_ID, META_API_VERSION, ANTHROPIC_API_KEY.\n2. Run the manual trigger, or attach a Schedule trigger for daily monitoring.\n\n### Customization\nSet CONVERSION_EVENTS and ALERT_BELOW in Config. To track decay over time, add a node that appends the daily scores to Google Sheets or a database and diff against the prior run. Swap stdout for Slack or email to auto-alert.\n\nBuilt by **nocode.expert** - done-for-you automation & tracking. https://nocode.expert"
},
"typeVersion": 1
},
{
"id": "93a3baa8-0ace-4691-b605-07f49c8d53b9",
"name": "Section: Read match stats",
"type": "n8n-nodes-base.stickyNote",
"position": [
11168,
1008
],
"parameters": {
"color": 7,
"width": 544,
"height": 288,
"content": "## 1. Read match stats\nDiscover pixels and pull per-event PII coverage from the Graph API."
},
"typeVersion": 1
},
{
"id": "6872c879-91f5-472b-a2f3-0a12936a59f3",
"name": "Section: Score & explain",
"type": "n8n-nodes-base.stickyNote",
"position": [
11728,
1008
],
"parameters": {
"color": 7,
"width": 1152,
"height": 288,
"content": "## 2. Score & explain\nScore match quality, then Claude explains gaps and fixes."
},
"typeVersion": 1
},
{
"id": "ae042b40-645f-4061-8a05-36ebea34283d",
"name": "Section: Report",
"type": "n8n-nodes-base.stickyNote",
"position": [
12944,
992
],
"parameters": {
"color": 7,
"width": 396,
"height": 320,
"content": "## 3. Report\nReport the EMQ scorecard and alerts (swap for Slack/email)."
},
"typeVersion": 1
},
{
"id": "5bd641d3-f6f4-45b8-83a8-177206256cb6",
"name": "Run manually",
"type": "n8n-nodes-base.manualTrigger",
"position": [
10976,
1136
],
"parameters": {},
"typeVersion": 1
},
{
"id": "f52344d6-8efa-4553-a353-5da655ea7e50",
"name": "Set config: conversion events, alert threshold",
"type": "n8n-nodes-base.code",
"position": [
11216,
1136
],
"parameters": {
"jsCode": "// CONVERSION_EVENTS are weighted heavier because match quality matters most for\n// the events you optimize on. ALERT_BELOW is the score that triggers an alert.\n//\n// DATASET_IDS: leave empty [] to auto-discover and surface EVERY active pixel/\n// dataset on the account, or list specific IDs to monitor only those.\nreturn [{ json: {\n DATASET_IDS: [], // e.g. ['YOUR_DATASET_ID_1', 'YOUR_DATASET_ID_2'] to pick specific datasets\n PIXEL_LIMIT: 50, // max pixels to check when auto-discovering\n ACTIVE_DAYS: 14, // when auto-discovering, only score pixels fired within this many days\n ALERT_BELOW: 70, // score under this = alert\n CONVERSION_EVENTS: ['SubmitApplication', 'Lead', 'Purchase', 'CompleteRegistration', 'Optin-Start', 'Subscribe', 'StartTrial', 'AddPaymentInfo'],\n} }];"
},
"typeVersion": 2
},
{
"id": "0b9b3686-fe55-4ae0-865a-1b1b10f7cd39",
"name": "Build AI match-quality summary prompt",
"type": "n8n-nodes-base.code",
"position": [
12304,
1136
],
"parameters": {
"jsCode": "// Ask Claude to explain the biggest match-quality gaps and how to fix them.\nconst { pixels, cfg } = $json;\nconst worst = pixels.filter((p) => p.score !== null && p.score < cfg.ALERT_BELOW);\nconst system = 'You are a server-side tracking specialist. You are given Meta pixel event match-quality data (PII coverage per event; conversion events matter most). For each low-scoring pixel, explain the risk in one line and give one concrete fix (e.g. add hashed email/phone to CAPI payloads, pass fbc/fbp, enable Advanced Matching). Be specific and terse. Return ONLY valid JSON.';\nconst user = 'Alert threshold: ' + cfg.ALERT_BELOW + '. Pixels:\\n' + JSON.stringify(pixels) + '\\n\\nReturn JSON exactly: {\"summary\":\"one sentence\",\"pixels\":[{\"name\":\"\",\"score\":0,\"risk\":\"\",\"fix\":\"\"}]}';\nconst body = { model: 'claude-haiku-4-5', max_tokens: 1200, system, messages: [{ role: 'user', content: user }] };\nreturn [{ json: { body, _ctx: $json } }];"
},
"typeVersion": 2
},
{
"id": "a6cc4bec-19b9-4a3b-a832-416639998676",
"name": "Summarize match-quality gaps with Claude AI",
"type": "n8n-nodes-base.httpRequest",
"position": [
12624,
1136
],
"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": "d0a4974b-eb52-41c3-9edb-bb65eb1d7ba2",
"name": "Print EMQ scorecard & alerts",
"type": "n8n-nodes-base.code",
"position": [
13024,
1136
],
"parameters": {
"jsCode": "// Print the EMQ scorecard and AI recommendations to stdout.\nconst ctx = $node['Score event match quality per pixel'].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 fixOf = {}; (parsed.pixels || []).forEach((p) => { fixOf[p.name] = p; });\n\nconst out = [];\nout.push('');\nout.push('==========================================================');\nout.push(' META CAPI EVENT MATCH QUALITY MONITOR | nocode.expert');\nout.push('==========================================================');\nout.push(parsed.summary || '');\nout.push('Pixels checked: ' + ctx.checked + ' | Alert below: ' + ctx.cfg.ALERT_BELOW);\nout.push('');\nfor (const p of ctx.pixels) {\n const flag = p.score !== null && p.score < ctx.cfg.ALERT_BELOW ? ' <-- ALERT' : '';\n out.push('[' + (p.score === null ? ' n/a' : String(p.score).padStart(3)) + '/100] ' + p.name + flag);\n const conv = p.events.filter((x) => x.is_conversion);\n for (const c of conv.slice(0, 3)) out.push(' ' + c.name + ': ' + c.pii_pct + '% PII on ' + c.events + ' events');\n const f = fixOf[p.name];\n if (f && f.fix) out.push(' FIX: ' + f.fix);\n}\nout.push('==========================================================');\nconsole.log(out.join('\\n'));\nreturn [{ json: { scorecard: ctx.pixels, summary: parsed.summary } }];"
},
"typeVersion": 2
},
{
"id": "110b68f4-fb80-473c-9fe0-093a55c19280",
"name": "List active pixels (Meta)",
"type": "n8n-nodes-base.facebookGraphApi",
"position": [
11392,
1136
],
"parameters": {
"edge": "adspixels",
"node": "={{ ($env.META_AD_ACCOUNT_ID || '').trim() }}",
"options": {
"queryParameters": {
"parameter": [
{
"name": "fields",
"value": "id,name,last_fired_time"
},
{
"name": "limit",
"value": "100"
}
]
}
},
"graphApiVersion": "v21.0"
},
"typeVersion": 1
},
{
"id": "00bb6a32-57ca-4ca4-a581-040c986b7ece",
"name": "Select active pixels",
"type": "n8n-nodes-base.code",
"position": [
11584,
1136
],
"parameters": {
"jsCode": "const cfg = $('Set config: conversion events, alert threshold').first().json;\nconst now = Date.now();\nconst list = $json.data || [];\nlet active;\nif (Array.isArray(cfg.DATASET_IDS) && cfg.DATASET_IDS.length) {\n const want = new Set(cfg.DATASET_IDS.map(String));\n active = list.filter(p => want.has(String(p.id)));\n} else {\n active = list.filter(p => p.last_fired_time && (now - new Date(p.last_fired_time).getTime()) < cfg.ACTIVE_DAYS*864e5).slice(0, cfg.PIXEL_LIMIT);\n}\nreturn active.map(p => ({ json: { id: p.id, name: p.name } }));"
},
"typeVersion": 2
},
{
"id": "32c6e534-9391-4c33-87fb-e1a899a0fcb5",
"name": "Pixel PII stats (Meta)",
"type": "n8n-nodes-base.facebookGraphApi",
"position": [
11792,
1136
],
"parameters": {
"edge": "stats",
"node": "={{ $json.id }}",
"options": {
"queryParameters": {
"parameter": [
{
"name": "aggregation",
"value": "had_pii"
}
]
}
},
"graphApiVersion": "v21.0"
},
"typeVersion": 1
},
{
"id": "c1242e48-5947-46f2-a885-5383774405f0",
"name": "Score event match quality per pixel",
"type": "n8n-nodes-base.code",
"position": [
12016,
1136
],
"parameters": {
"jsCode": "const cfg = $('Set config: conversion events, alert threshold').first().json;\nconst pixelsMeta = $('Select active pixels').all();\nconst CONV = new Set(cfg.CONVERSION_EVENTS);\nconst responses = $input.all();\nconst pixels = [];\nresponses.forEach((item, i) => {\n const p = pixelsMeta[i].json;\n const rows = (((item.json.data||[{}])[0]||{}).data) || [];\n const ev = {};\n for (const r of rows) { const e = (ev[r.event] = ev[r.event] || { has:0, total:0 }); const c = Number(r.count)||0; e.total += c; if (r.value==='has_pii') e.has += c; }\n const events = Object.entries(ev).map(([name,c]) => ({ name, events:c.total, pii_pct: c.total? Math.round(100*c.has/c.total):0, is_conversion: CONV.has(name) })).sort((a,b)=>b.events-a.events);\n let wSum=0,wPii=0;\n for (const e of events){ const w=(e.is_conversion?3:1)*e.events; wSum+=w; wPii+=w*e.pii_pct; }\n const score = wSum? Math.round(wPii/wSum): null;\n if (events.length) pixels.push({ id:p.id, name:p.name, score, events });\n});\npixels.sort((a,b)=>(a.score??101)-(b.score??101));\nreturn [{ json: { pixels, checked: pixelsMeta.length, cfg } }];"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"executionOrder": "v1"
},
"versionId": "ab3dc5fa-9c1e-4798-a959-cdf9cdf2bbaa",
"connections": {
"Run manually": {
"main": [
[
{
"node": "Set config: conversion events, alert threshold",
"type": "main",
"index": 0
}
]
]
},
"Select active pixels": {
"main": [
[
{
"node": "Pixel PII stats (Meta)",
"type": "main",
"index": 0
}
]
]
},
"Pixel PII stats (Meta)": {
"main": [
[
{
"node": "Score event match quality per pixel",
"type": "main",
"index": 0
}
]
]
},
"List active pixels (Meta)": {
"main": [
[
{
"node": "Select active pixels",
"type": "main",
"index": 0
}
]
]
},
"Score event match quality per pixel": {
"main": [
[
{
"node": "Build AI match-quality summary prompt",
"type": "main",
"index": 0
}
]
]
},
"Build AI match-quality summary prompt": {
"main": [
[
{
"node": "Summarize match-quality gaps with Claude AI",
"type": "main",
"index": 0
}
]
]
},
"Summarize match-quality gaps with Claude AI": {
"main": [
[
{
"node": "Print EMQ scorecard & alerts",
"type": "main",
"index": 0
}
]
]
},
"Set config: conversion events, alert threshold": {
"main": [
[
{
"node": "List active pixels (Meta)",
"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 runs on demand to monitor Meta Conversions API Event Match Quality across the pixels in a Meta ad account, calculates a weighted 0–100 match-quality score from Graph API stats, and uses Anthropic Claude to summarize risks and fixes for any pixels below an alert…
Source: https://n8n.io/workflows/17011/ — 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 n8n workflow automates the process of uploading video and image advertisements to Meta Ads Manager via the Meta Graph API (Facebook Ads) directly from Google Sheets and Google Drive. The workflow
This n8n workflow automates the creation and publishing of Instagram carousel posts by downloading images from Google Drive, uploading them to Cloudinary for hosting, and publishing them as a coordina
This workflow pulls the last 30 days of Meta Ads insights across multiple breakdowns, calculates an account cost-per-result benchmark, flags high-waste delivery slices, and uses Anthropic Claude to ra
This workflow pulls ad spend and platform-reported revenue from Meta (with placeholders for Google Ads and TikTok), combines it with your actual store revenue and new-customer count, calculates blende
This template is designed for Marketing Managers, Performance Marketers, and Ad Ops professionals who want to automate Facebook ad creation using structured data in Google Sheets. It’s ideal for teams