{"name":"PMax optimization workflow","nodes":[{"id":"9f01c1d2-18a0-4b31-8c44-556677889901","name":"Every Morning 7am","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-200,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"9f02c1d2-18a0-4b31-8c44-556677889902","name":"Set PMax Guardrails","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[60,300],"parameters":{"assignments":{"assignments":[{"id":"g1","name":"customer_id","value":"1234567890","type":"string"},{"id":"g2","name":"target_roas","value":3.2,"type":"number"},{"id":"g3","name":"target_cpa","value":42,"type":"number"},{"id":"g4","name":"min_cost_micros","value":150000000,"type":"number"},{"id":"g5","name":"monthly_budget","value":45000,"type":"number"}]},"options":{}}},{"id":"9f03c1d2-18a0-4b31-8c44-556677889903","name":"Fetch Asset Group Performance","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[320,300],"parameters":{"method":"POST","url":"=https://googleads.googleapis.com/v18/customers/{{ $json.customer_id }}/googleAds:searchStream","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ query: \"SELECT campaign.id, campaign.name, campaign.advertising_channel_type, asset_group.id, asset_group.name, asset_group.status, asset_group.ad_strength, metrics.cost_micros, metrics.conversions, metrics.conversions_value, metrics.impressions, metrics.clicks FROM asset_group WHERE campaign.advertising_channel_type = 'PERFORMANCE_MAX' AND segments.date DURING LAST_30_DAYS\" }) }}","options":{}}},{"id":"9f04c1d2-18a0-4b31-8c44-556677889904","name":"Fetch Asset Level Metrics","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[580,300],"parameters":{"method":"POST","url":"=https://googleads.googleapis.com/v18/customers/{{ $(\"Set PMax Guardrails\").item.json.customer_id }}/googleAds:searchStream","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ query: \"SELECT asset_group.id, asset.id, asset.name, asset.type, asset_group_asset.field_type, asset_group_asset.performance_label, metrics.impressions, metrics.clicks, metrics.conversions, metrics.cost_micros FROM asset_group_asset WHERE segments.date DURING LAST_30_DAYS\" }) }}","options":{}}},{"id":"9f05c1d2-18a0-4b31-8c44-556677889905","name":"Score Asset Groups","type":"n8n-nodes-base.code","typeVersion":2,"position":[840,300],"parameters":{"jsCode":"// ---- Google Ads PMax: asset-group health scoring -------------------------\nconst guard = $('Set PMax Guardrails').first().json;\nconst TARGET_ROAS = guard.target_roas;          // e.g. 3.2\nconst TARGET_CPA  = guard.target_cpa;           // e.g. 42\nconst MIN_COST    = guard.min_cost_micros;      // 150 USD in micros = statistical floor\nconst MONTHLY_BUDGET = guard.monthly_budget;\n\nconst STRENGTH_RANK = { UNSPECIFIED: 0, UNKNOWN: 0, PENDING: 1, NO_ADS: 0, POOR: 1, AVERAGE: 2, GOOD: 3, EXCELLENT: 4 };\n\n// searchStream returns chunks: [{ results: [...] }]. Flatten + tolerate a flat shape.\nconst rows = [];\nfor (const item of $input.all()) {\n  const chunk = item.json;\n  const results = chunk.results || (Array.isArray(chunk) ? chunk : [chunk]);\n  for (const r of results) if (r && r.assetGroup) rows.push(r);\n}\n\n// Dedupe by assetGroup.id (searchStream can repeat rows across chunks) and sum metrics.\nconst byGroup = new Map();\nfor (const r of rows) {\n  const id = String(r.assetGroup.id);\n  const m = r.metrics || {};\n  const acc = byGroup.get(id) || {\n    asset_group_id: id,\n    asset_group_name: r.assetGroup.name,\n    ad_strength: r.assetGroup.adStrength || 'UNKNOWN',\n    status: r.assetGroup.status || 'ENABLED',\n    campaign_id: String(r.campaign?.id || ''),\n    campaign_name: r.campaign?.name || '',\n    cost_micros: 0, conversions: 0, conversions_value: 0, impressions: 0, clicks: 0,\n  };\n  acc.cost_micros       += Number(m.costMicros || 0);\n  acc.conversions       += Number(m.conversions || 0);\n  acc.conversions_value += Number(m.conversionsValue || 0);\n  acc.impressions       += Number(m.impressions || 0);\n  acc.clicks            += Number(m.clicks || 0);\n  byGroup.set(id, acc);\n}\n\nconst groups = [...byGroup.values()].map(g => {\n  const cost = g.cost_micros / 1e6;\n  const roas = cost > 0 ? g.conversions_value / cost : 0;\n  const cpa  = g.conversions > 0 ? cost / g.conversions : null;\n  const ctr  = g.impressions > 0 ? g.clicks / g.impressions : 0;\n  const cvr  = g.clicks > 0 ? g.conversions / g.clicks : 0;\n\n  // Health score 0-100: 55% efficiency vs target, 25% ad strength, 20% funnel quality.\n  const roasIdx     = Math.min(roas / TARGET_ROAS, 1.5) / 1.5;                 // 0..1\n  const cpaIdx      = cpa === null ? 0 : Math.min(TARGET_CPA / cpa, 1.5) / 1.5; // 0..1\n  const efficiency  = g.conversions >= 1 ? (roasIdx * 0.6 + cpaIdx * 0.4) : 0;\n  const strengthIdx = STRENGTH_RANK[g.ad_strength] / 4;\n  const funnelIdx   = Math.min(ctr / 0.012, 1) * 0.5 + Math.min(cvr / 0.02, 1) * 0.5;\n  const score = Math.round((efficiency * 55 + strengthIdx * 25 + funnelIdx * 20));\n\n  const spendShare = MONTHLY_BUDGET > 0 ? cost / MONTHLY_BUDGET : 0;\n  // Waste = spend that would be freed if this group merely hit target ROAS.\n  const wasted = roas < TARGET_ROAS ? Math.max(0, cost - (g.conversions_value / TARGET_ROAS)) : 0;\n\n  const reasons = [];\n  if (cost >= MIN_COST / 1e6 && roas < TARGET_ROAS * 0.7) reasons.push(`ROAS ${roas.toFixed(2)} vs target ${TARGET_ROAS}`);\n  if (cpa !== null && cpa > TARGET_CPA * 1.3) reasons.push(`CPA $${cpa.toFixed(0)} vs target $${TARGET_CPA}`);\n  if (STRENGTH_RANK[g.ad_strength] <= 1) reasons.push(`ad strength ${g.ad_strength}`);\n  if (ctr < 0.006 && g.impressions > 20000) reasons.push(`CTR ${(ctr * 100).toFixed(2)}% below floor`);\n  if (g.conversions < 1 && cost >= MIN_COST / 1e6) reasons.push('zero conversions on meaningful spend');\n\n  return {\n    ...g,\n    cost: Number(cost.toFixed(2)),\n    roas: Number(roas.toFixed(2)),\n    cpa: cpa === null ? null : Number(cpa.toFixed(2)),\n    ctr: Number((ctr * 100).toFixed(3)),\n    cvr: Number((cvr * 100).toFixed(3)),\n    health_score: score,\n    spend_share: Number((spendShare * 100).toFixed(1)),\n    wasted_spend: Number(wasted.toFixed(2)),\n    significant: cost >= MIN_COST / 1e6,\n    reasons,\n    verdict: score >= 65 ? 'SCALE' : score >= 45 ? 'HOLD' : 'REBUILD',\n  };\n});\n\n// Rank worst first so the loop below works on the biggest bleeders.\ngroups.sort((a, b) => (b.wasted_spend - a.wasted_spend) || (a.health_score - b.health_score));\nreturn groups.map(g => ({ json: g }));"}},{"id":"9f06c1d2-18a0-4b31-8c44-556677889906","name":"Only Significant Spend","type":"n8n-nodes-base.filter","typeVersion":2,"position":[1100,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"f1","leftValue":"={{ $json.significant }}","rightValue":true,"operator":{"type":"boolean","operation":"true"}}]},"options":{}}},{"id":"9f07c1d2-18a0-4b31-8c44-556677889907","name":"Under-performing?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1360,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"i1","leftValue":"={{ $json.health_score }}","rightValue":65,"operator":{"type":"number","operation":"lt"}}]},"options":{}}},{"id":"9f08c1d2-18a0-4b31-8c44-556677889908","name":"Log Healthy Groups","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[1620,80],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"PMax Health Log"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"9f09c1d2-18a0-4b31-8c44-556677889909","name":"Loop Weak Asset Groups","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[1620,300],"parameters":{"batchSize":1,"options":{}}},{"id":"9f10c1d2-18a0-4b31-8c44-556677889910","name":"Diagnose Weak Assets","type":"n8n-nodes-base.code","typeVersion":2,"position":[1880,520],"parameters":{"jsCode":"// ---- Which individual assets are dragging this asset group down? ---------\nconst group = $input.first().json;\n\nconst LABEL_RANK = { BEST: 3, GOOD: 2, LOW: 1, LEARNING: 0, PENDING: 0, UNKNOWN: 0, UNSPECIFIED: 0 };\n// Google's minimum viable PMax asset group inventory.\nconst MIN_BY_FIELD = { HEADLINE: 3, LONG_HEADLINE: 1, DESCRIPTION: 2, MARKETING_IMAGE: 1, SQUARE_MARKETING_IMAGE: 1, LOGO: 1, YOUTUBE_VIDEO: 1 };\n\nconst rows = [];\nfor (const item of $('Fetch Asset Level Metrics').all()) {\n  const chunk = item.json;\n  const results = chunk.results || (Array.isArray(chunk) ? chunk : [chunk]);\n  for (const r of results) if (r && r.assetGroup) rows.push(r);\n}\n\n// Keep only assets in THIS group, dedupe by asset id + field type.\nconst seen = new Set();\nconst assets = [];\nfor (const r of rows) {\n  if (String(r.assetGroup.id) !== group.asset_group_id) continue;\n  const field = r.assetGroupAsset?.fieldType || 'UNKNOWN';\n  const key = `${r.asset?.id}:${field}`;\n  if (seen.has(key)) continue;\n  seen.add(key);\n  const m = r.metrics || {};\n  const impressions = Number(m.impressions || 0);\n  const clicks = Number(m.clicks || 0);\n  const conversions = Number(m.conversions || 0);\n  const label = r.assetGroupAsset?.performanceLabel || 'PENDING';\n  assets.push({\n    asset_id: String(r.asset?.id || ''),\n    asset_name: r.asset?.name || '(unnamed)',\n    asset_type: r.asset?.type || 'UNKNOWN',\n    field_type: field,\n    performance_label: label,\n    label_rank: LABEL_RANK[label] ?? 0,\n    impressions, clicks, conversions,\n    ctr: impressions > 0 ? Number((clicks / impressions * 100).toFixed(3)) : 0,\n  });\n}\n\n// Replace: LOW-rated assets, plus anything with real impressions and no clicks.\nconst replace = assets.filter(a =>\n  a.performance_label === 'LOW' ||\n  (a.impressions >= 5000 && a.clicks === 0) ||\n  (a.impressions >= 20000 && a.ctr < 0.4)\n);\n\n// Count what survives per field type, and how many new assets we must brief.\nconst keptByField = {};\nfor (const a of assets) {\n  if (replace.some(r => r.asset_id === a.asset_id && r.field_type === a.field_type)) continue;\n  keptByField[a.field_type] = (keptByField[a.field_type] || 0) + 1;\n}\nconst gaps = Object.entries(MIN_BY_FIELD)\n  .map(([field, min]) => ({ field_type: field, have: keptByField[field] || 0, need: Math.max(0, min - (keptByField[field] || 0)) }))\n  .filter(g => g.need > 0);\n\n// Winners we keep as the tone reference for the LLM brief.\nconst winners = assets.filter(a => a.label_rank >= 2).sort((a, b) => b.label_rank - a.label_rank || b.ctr - a.ctr).slice(0, 5);\n\n// 30d pacing projection if we fix the group back to target ROAS.\nconst dailySpend = group.cost / 30;\nconst projected_monthly_value = Number((dailySpend * 30 * $('Set PMax Guardrails').first().json.target_roas).toFixed(2));\nconst recoverable = Number((projected_monthly_value - group.conversions_value).toFixed(2));\n\nreturn [{ json: {\n  ...group,\n  assets_total: assets.length,\n  assets_to_replace: replace.map(a => ({ asset_id: a.asset_id, field_type: a.field_type, label: a.performance_label, ctr: a.ctr })),\n  replace_count: replace.length,\n  inventory_gaps: gaps,\n  new_assets_needed: gaps.reduce((s, g) => s + g.need, 0) + replace.length,\n  winner_examples: winners.map(w => w.asset_name),\n  projected_monthly_value,\n  recoverable_value: recoverable,\n} }];"}},{"id":"9f11c1d2-18a0-4b31-8c44-556677889911","name":"Needs New Creative?","type":"n8n-nodes-base.if","typeVersion":2,"position":[2140,520],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"i2","leftValue":"={{ $json.new_assets_needed }}","rightValue":0,"operator":{"type":"number","operation":"gt"}}]},"options":{}}},{"id":"9f12c1d2-18a0-4b31-8c44-556677889912","name":"Flag For Budget Reallocation","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[2400,740],"parameters":{"assignments":{"assignments":[{"id":"s1","name":"asset_group_name","value":"={{ $json.asset_group_name }}","type":"string"},{"id":"s2","name":"action","value":"REALLOCATE_BUDGET","type":"string"},{"id":"s3","name":"brief","value":"=Assets are fine ({{ $json.assets_total }} live, none rated LOW). Health {{ $json.health_score }} is driven by targeting/budget, not creative. Shift ${{ $json.wasted_spend }} of monthly spend to the top-scoring asset group.","type":"string"},{"id":"s4","name":"wasted_spend","value":"={{ $json.wasted_spend }}","type":"number"},{"id":"s5","name":"asset_group_id","value":"={{ $json.asset_group_id }}","type":"string"},{"id":"s6","name":"campaign_name","value":"={{ $json.campaign_name }}","type":"string"},{"id":"s7","name":"health_score","value":"={{ $json.health_score }}","type":"number"},{"id":"s8","name":"roas","value":"={{ $json.roas }}","type":"number"},{"id":"s9","name":"new_assets_needed","value":0,"type":"number"},{"id":"s10","name":"priority","value":"={{ $json.wasted_spend > 500 ? 'P1' : $json.wasted_spend > 150 ? 'P2' : 'P3' }}","type":"string"},{"id":"s11","name":"needs_human_review","value":"={{ $json.wasted_spend > 500 }}","type":"boolean"}]},"options":{}}},{"id":"9f13c1d2-18a0-4b31-8c44-556677889913","name":"Brief Replacement Assets","type":"@n8n/n8n-nodes-langchain.openAi","typeVersion":1.8,"position":[2400,520],"parameters":{"modelId":{"__rl":true,"value":"gpt-4o-mini","mode":"list"},"messages":{"values":[{"role":"system","content":"You are a performance creative strategist for Google Performance Max. Return a tight production brief: for each requested field type, give the exact copy or a one-line shot description. No preamble, no explanation of PMax."},{"content":"=Asset group \"{{ $json.asset_group_name }}\" in campaign \"{{ $json.campaign_name }}\".\n30d: spend ${{ $json.cost }}, ROAS {{ $json.roas }}, CPA {{ $json.cpa }}, ad strength {{ $json.ad_strength }}, health score {{ $json.health_score }}/100.\nDiagnosis: {{ $json.reasons.join(\"; \") }}\nAssets to replace: {{ JSON.stringify($json.assets_to_replace) }}\nInventory gaps to fill: {{ JSON.stringify($json.inventory_gaps) }}\nTop performers to echo in tone: {{ $json.winner_examples.join(\" | \") }}\nProduce {{ $json.new_assets_needed }} new assets covering the gaps above."}]},"options":{}}},{"id":"9f14c1d2-18a0-4b31-8c44-556677889914","name":"Assemble Action Plan","type":"n8n-nodes-base.code","typeVersion":2,"position":[2660,520],"parameters":{"jsCode":"// ---- Merge the LLM brief back onto the diagnosed group -------------------\nconst diag = $('Diagnose Weak Assets').first().json;\nconst llm = $input.first().json;\nconst brief = llm.message?.content || llm.content || llm.text || '';\n\n// Cheap sanity check: did the model actually cover every gap field type?\nconst covered = diag.inventory_gaps.filter(g => new RegExp(g.field_type.replace(/_/g, '[ _]?'), 'i').test(brief));\nconst coverage = diag.inventory_gaps.length === 0 ? 1 : covered.length / diag.inventory_gaps.length;\n\nconst priority = diag.wasted_spend > 500 ? 'P1' : diag.wasted_spend > 150 ? 'P2' : 'P3';\n\nreturn [{ json: {\n  asset_group_id: diag.asset_group_id,\n  asset_group_name: diag.asset_group_name,\n  campaign_name: diag.campaign_name,\n  action: diag.verdict === 'REBUILD' ? 'REBUILD_ASSET_GROUP' : 'REFRESH_ASSETS',\n  priority,\n  health_score: diag.health_score,\n  roas: diag.roas,\n  cpa: diag.cpa,\n  cost: diag.cost,\n  wasted_spend: diag.wasted_spend,\n  recoverable_value: diag.recoverable_value,\n  replace_count: diag.replace_count,\n  new_assets_needed: diag.new_assets_needed,\n  gap_coverage: Number((coverage * 100).toFixed(0)),\n  needs_human_review: coverage < 1 || diag.new_assets_needed > 8,\n  brief,\n} }];"}},{"id":"9f15c1d2-18a0-4b31-8c44-556677889915","name":"Merge Plan Rows","type":"n8n-nodes-base.merge","typeVersion":3,"position":[2920,630],"parameters":{"mode":"append","options":{}}},{"id":"9f16c1d2-18a0-4b31-8c44-556677889916","name":"Queue Creative Task","type":"n8n-nodes-base.airtable","typeVersion":2.1,"position":[3180,630],"parameters":{"operation":"create","base":{"__rl":true,"value":"appXXXXXXXX","mode":"id"},"table":{"__rl":true,"value":"tblXXXXXXXX","mode":"id"},"columns":{"mappingMode":"autoMapInputData","value":{}},"options":{}}},{"id":"9f17c1d2-18a0-4b31-8c44-556677889917","name":"Build Daily Report","type":"n8n-nodes-base.code","typeVersion":2,"position":[1880,80],"parameters":{"jsCode":"// ---- Runs once the loop is done: roll every plan row into one report -----\n// The splitInBatches \"done\" output emits everything that was fed back into the\n// loop, so $input.all() is the full set of plan rows - one per weak asset group.\nconst plans = $input.all().map(i => i.json).filter(p => p && p.asset_group_name);\nconst totalWaste = plans.reduce((s, p) => s + (p.wasted_spend || 0), 0);\nconst totalAssets = plans.reduce((s, p) => s + (p.new_assets_needed || 0), 0);\nconst order = { P1: 0, P2: 1, P3: 2 };\nplans.sort((a, b) => (order[a.priority] ?? 9) - (order[b.priority] ?? 9) || b.wasted_spend - a.wasted_spend);\n\nconst lines = plans.map(p =>\n  `[${p.priority}] ${p.asset_group_name} — health ${p.health_score}/100, ROAS ${p.roas}, $${p.wasted_spend} wasted/mo → ${p.action || 'REALLOCATE_BUDGET'} (${p.new_assets_needed || 0} new assets)`\n);\n\nconst html = [\n  `<h2>PMax optimization plan</h2>`,\n  `<p>${plans.length} asset groups flagged · $${totalWaste.toFixed(0)} monthly waste identified · ${totalAssets} new assets briefed</p>`,\n  '<ul>' + lines.map(l => `<li>${l}</li>`).join('') + '</ul>',\n].join('');\n\nreturn [{ json: {\n  flagged: plans.length,\n  total_waste: Number(totalWaste.toFixed(2)),\n  total_new_assets: totalAssets,\n  needs_review: plans.filter(p => p.needs_human_review).length,\n  summary: lines.join('\\n'),\n  html,\n} }];"}},{"id":"9f18c1d2-18a0-4b31-8c44-556677889918","name":"Email PMax Report","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[2140,80],"parameters":{"sendTo":"ppc@brand.com","subject":"=PMax optimization plan {{ $now.format('yyyy-LL-dd') }} — {{ $json.flagged }} asset groups","message":"={{ $json.html }}","options":{}}},{"id":"9f19c1d2-18a0-4b31-8c44-556677889919","name":"Alert PPC Channel","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2400,80],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#ppc-alerts","mode":"name"},"text":"=🧩 PMax: {{ $('Build Daily Report').first().json.flagged }} weak asset groups · ${{ $('Build Daily Report').first().json.total_waste }}/mo waste · {{ $('Build Daily Report').first().json.total_new_assets }} assets briefed · {{ $('Build Daily Report').first().json.needs_review }} need human review","otherOptions":{}}},{"id":"9f20c1d2-18a0-4b31-8c44-556677889920","name":"Note Ingest","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[40,20],"parameters":{"content":"## 1 · PULL PMAX DATA\nDaily GAQL pull of every Performance Max asset group (30d) plus asset-level performance labels, then a health score: 55% efficiency vs target ROAS/CPA, 25% ad strength, 20% CTR/CVR quality.","height":300,"width":460,"color":4}},{"id":"9f21c1d2-18a0-4b31-8c44-556677889921","name":"Note Diagnose","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1840,800],"parameters":{"content":"## 2 · DIAGNOSE & BRIEF\nPer weak group: find LOW-label assets and zero-click impressions, compute inventory gaps vs Google minimums, then have the LLM brief exactly the missing assets. No creative gaps = budget reallocation instead.","height":300,"width":460,"color":5}},{"id":"9f22c1d2-18a0-4b31-8c44-556677889922","name":"Note Report","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1840,-260],"parameters":{"content":"## 3 · QUEUE & REPORT\nEvery plan row lands in Airtable for the design team, then the loop-done branch rolls it into a prioritised P1/P2/P3 email + Slack digest with total monthly waste.","height":280,"width":460,"color":3}}],"connections":{"Every Morning 7am":{"main":[[{"node":"Set PMax Guardrails","type":"main","index":0}]]},"Set PMax Guardrails":{"main":[[{"node":"Fetch Asset Group Performance","type":"main","index":0}]]},"Fetch Asset Group Performance":{"main":[[{"node":"Fetch Asset Level Metrics","type":"main","index":0}]]},"Fetch Asset Level Metrics":{"main":[[{"node":"Score Asset Groups","type":"main","index":0}]]},"Score Asset Groups":{"main":[[{"node":"Only Significant Spend","type":"main","index":0}]]},"Only Significant Spend":{"main":[[{"node":"Under-performing?","type":"main","index":0}]]},"Under-performing?":{"main":[[{"node":"Loop Weak Asset Groups","type":"main","index":0}],[{"node":"Log Healthy Groups","type":"main","index":0}]]},"Loop Weak Asset Groups":{"main":[[{"node":"Build Daily Report","type":"main","index":0}],[{"node":"Diagnose Weak Assets","type":"main","index":0}]]},"Diagnose Weak Assets":{"main":[[{"node":"Needs New Creative?","type":"main","index":0}]]},"Needs New Creative?":{"main":[[{"node":"Brief Replacement Assets","type":"main","index":0}],[{"node":"Flag For Budget Reallocation","type":"main","index":0}]]},"Brief Replacement Assets":{"main":[[{"node":"Assemble Action Plan","type":"main","index":0}]]},"Assemble Action Plan":{"main":[[{"node":"Merge Plan Rows","type":"main","index":0}]]},"Flag For Budget Reallocation":{"main":[[{"node":"Merge Plan Rows","type":"main","index":1}]]},"Merge Plan Rows":{"main":[[{"node":"Queue Creative Task","type":"main","index":0}]]},"Queue Creative Task":{"main":[[{"node":"Loop Weak Asset Groups","type":"main","index":0}]]},"Build Daily Report":{"main":[[{"node":"Email PMax Report","type":"main","index":0}]]},"Email PMax Report":{"main":[[{"node":"Alert PPC Channel","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}