{"name":"AI UGC ads factory workflow","nodes":[{"id":"aa11bb22-0001-4c33-8d44-ee55ff660000","name":"Daily Factory Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[40,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"aa11bb22-0002-4c33-8d44-ee55ff660000","name":"Load Product Backlog","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[300,300],"parameters":{"operation":"read","documentId":{"__rl":true,"value":"UGC_FACTORY_SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Product Backlog"},"options":{}}},{"id":"aa11bb22-0003-4c33-8d44-ee55ff660000","name":"Build Concept Briefs","type":"n8n-nodes-base.code","typeVersion":2,"position":[560,300],"parameters":{"jsCode":"// ---- UGC CONCEPT BRIEF BUILDER -------------------------------------------\n// Input: rows from the \"Product Backlog\" sheet, one row per product SKU.\n// Columns: product_handle, product_name, price, cogs, hero_benefit,\n//          pain_points (pipe separated), creator_personas (pipe separated),\n//          spend_14d, purchase_roas_14d, impressions_14d, live_creatives,\n//          used_angle_keys (pipe separated hashes already produced)\nconst DAILY_CONCEPT_QUOTA = 24;      // scripts we want to ship into the queue per run\nconst MAX_PER_PRODUCT     = 6;       // don't let one SKU eat the whole day\nconst CREATIVE_SATURATION = 12;      // > this many live creatives = deprioritise\n\nconst FORMATS = [\n  { id: 'problem_solution', runtime: 30, opener: 'pattern_interrupt' },\n  { id: 'three_reasons',    runtime: 34, opener: 'listicle' },\n  { id: 'unboxing_pov',     runtime: 22, opener: 'pov' },\n  { id: 'before_after',     runtime: 26, opener: 'transformation' },\n  { id: 'street_interview', runtime: 28, opener: 'question' },\n  { id: 'founder_reply',    runtime: 24, opener: 'comment_reply' }\n];\n\nconst rows = $input.all().map(i => i.json);\n\n// 1. priority score per product: proven ROAS + margin headroom - creative saturation\nconst scored = rows.map(r => {\n  const price = Number(r.price) || 0;\n  const cogs  = Number(r.cogs) || 0;\n  const roas  = Number(r.purchase_roas_14d) || 0;\n  const spend = Number(r.spend_14d) || 0;\n  const live  = Number(r.live_creatives) || 0;\n  const margin = price > 0 ? (price - cogs) / price : 0;\n\n  // confidence: a 0-1 ramp so a $40 test doesn't outrank a $4k proven SKU\n  const confidence = Math.min(1, spend / 2000);\n  const roasScore  = Math.min(1, roas / 3) * confidence + (1 - confidence) * 0.4;\n  const satPenalty = Math.min(0.5, Math.max(0, live - CREATIVE_SATURATION) / 24);\n  const priority   = Math.round((roasScore * 0.55 + margin * 0.45 - satPenalty) * 1000) / 1000;\n\n  return { row: r, priority: Math.max(0.05, priority), margin, roas, live };\n}).sort((a, b) => b.priority - a.priority);\n\n// 2. allocate the daily quota proportionally to priority\nconst total = scored.reduce((s, p) => s + p.priority, 0) || 1;\nscored.forEach(p => {\n  p.slots = Math.min(MAX_PER_PRODUCT, Math.max(1, Math.round((p.priority / total) * DAILY_CONCEPT_QUOTA)));\n});\n\n// 3. expand product x pain point x format, dedupe against angles already made\nconst key = s => String(s).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\nconst briefs = [];\nlet cursor = 0;\n\nfor (const p of scored) {\n  const r = p.row;\n  const pains    = String(r.pain_points || '').split('|').map(s => s.trim()).filter(Boolean);\n  const personas = String(r.creator_personas || '').split('|').map(s => s.trim()).filter(Boolean);\n  const used     = new Set(String(r.used_angle_keys || '').split('|').map(s => s.trim()).filter(Boolean));\n  if (!pains.length || !personas.length) continue;\n\n  let made = 0, attempt = 0;\n  while (made < p.slots && attempt < pains.length * FORMATS.length) {\n    const pain    = pains[attempt % pains.length];\n    const format  = FORMATS[Math.floor(attempt / pains.length) % FORMATS.length];\n    const persona = personas[(made + cursor) % personas.length];\n    attempt++;\n\n    const angleKey = [key(r.product_handle), key(pain), format.id].join('::');\n    if (used.has(angleKey)) continue;   // already produced this angle, skip\n    used.add(angleKey);\n    made++;\n\n    briefs.push({\n      angle_key: angleKey,\n      product_handle: r.product_handle,\n      product_name: r.product_name,\n      price: Number(r.price) || 0,\n      hero_benefit: r.hero_benefit,\n      pain_point: pain,\n      creator_persona: persona,\n      format: format.id,\n      opener_style: format.opener,\n      target_runtime_sec: format.runtime,\n      product_priority: p.priority,\n      product_roas_14d: p.roas,\n      margin_pct: Math.round(p.margin * 1000) / 10,\n      requested_at: new Date().toISOString()\n    });\n  }\n  cursor += 1;\n  if (briefs.length >= DAILY_CONCEPT_QUOTA) break;\n}\n\nreturn briefs.slice(0, DAILY_CONCEPT_QUOTA).map(b => ({ json: b }));"}},{"id":"aa11bb22-0004-4c33-8d44-ee55ff660000","name":"Batch Briefs","type":"n8n-nodes-base.splitInBatches","typeVersion":3,"position":[820,300],"parameters":{"batchSize":4,"options":{}}},{"id":"aa11bb22-0005-4c33-8d44-ee55ff660000","name":"Generate Hook Script Shotlist","type":"@n8n/n8n-nodes-langchain.openAi","typeVersion":1.8,"position":[1080,300],"parameters":{"modelId":{"__rl":true,"value":"gpt-4o-mini","mode":"list"},"messages":{"values":[{"role":"system","content":"You are a UGC direct response writer for short-form paid social. You write like a real customer talking to their phone camera, never like an ad. Return ONLY valid JSON with keys: hook (string, max 12 words), script (string, spoken word for word), cta (string), shot_list (array of objects with shot, duration_sec, direction). Never use the words cure, guaranteed, miracle, clinically proven, FDA approved."},{"content":"=Product: {{ $json.product_name }} ({{ $json.price }} USD)\nHero benefit: {{ $json.hero_benefit }}\nPain point to open on: {{ $json.pain_point }}\nCreator persona: {{ $json.creator_persona }}\nFormat: {{ $json.format }} / opener style: {{ $json.opener_style }}\nTarget runtime: {{ $json.target_runtime_sec }} seconds (~2.6 words per second)\n\nWrite one script. Include at least two concrete specifics (a number, a timeframe or a measurable change) and at least 6 distinct shots in the shot list."}]},"options":{}}},{"id":"aa11bb22-0006-4c33-8d44-ee55ff660000","name":"Score Scripts Against Rubric","type":"n8n-nodes-base.code","typeVersion":2,"position":[1340,300],"parameters":{"jsCode":"// ---- SCRIPT RUBRIC SCORER -------------------------------------------------\n// Parses the LLM JSON, then scores each script 0-100 against the UGC rubric.\n// Approve at >= 78. Everything below gets a diagnosis + rewrite instruction.\nconst APPROVE_AT = 78;\nconst BANNED = ['cure', 'guaranteed', 'miracle', 'clinically proven', 'fda approved', '100% safe', 'instantly heals'];\nconst WEAK_OPENERS = ['hi guys', 'hey guys', 'hello everyone', 'so basically', 'in this video', 'let me tell you'];\nconst WORDS_PER_SEC = 2.6;   // natural UGC read speed\n\nconst out = [];\nconst items = $input.all();\n// The LLM emits one item per brief in the current batch, in order, so pair by\n// index against the batch the loop node just released (NOT the first brief).\nconst batch = $('Batch Briefs').all();\nfor (let idx = 0; idx < items.length; idx++) {\n  const item = items[idx];\n  const brief = (batch[idx] || batch[batch.length - 1] || { json: {} }).json;\n  const raw = item.json.message?.content ?? item.json.content ?? item.json.text ?? '';\n\n  let parsed;\n  try {\n    parsed = typeof raw === 'object' ? raw : JSON.parse(String(raw).replace(/```json|```/g, '').trim());\n  } catch (e) {\n    out.push({ json: { ...brief, parse_error: true, total_score: 0, decision: 'reject',\n                       reject_reason: 'LLM did not return valid JSON' } });\n    continue;\n  }\n\n  const hook   = String(parsed.hook || '');\n  const script = String(parsed.script || '');\n  const shots  = Array.isArray(parsed.shot_list) ? parsed.shot_list : [];\n  const cta    = String(parsed.cta || '');\n  const blob   = (hook + ' ' + script + ' ' + cta).toLowerCase();\n\n  // 1. HOOK (0-10): short, concrete, has a number or a question, no throat clearing\n  const hookWords = hook.trim().split(/\\s+/).filter(Boolean).length;\n  let hookScore = 4;\n  if (hookWords > 0 && hookWords <= 12) hookScore += 3; else if (hookWords <= 18) hookScore += 1;\n  if (/\\d/.test(hook)) hookScore += 1.5;\n  if (/\\?/.test(hook)) hookScore += 1;\n  if (/\\b(stop|nobody|i was wrong|don't buy|this is why)\\b/i.test(hook)) hookScore += 1;\n  if (WEAK_OPENERS.some(w => hook.toLowerCase().startsWith(w))) hookScore -= 4;\n  hookScore = Math.max(0, Math.min(10, hookScore));\n\n  // 2. PACING (0-10): does the script actually fit the target runtime?\n  const scriptWords = script.trim().split(/\\s+/).filter(Boolean).length;\n  const estRuntime  = scriptWords / WORDS_PER_SEC;\n  const target      = Number(brief.target_runtime_sec) || 28;\n  const drift       = Math.abs(estRuntime - target) / target;\n  const pacingScore = Math.max(0, Math.min(10, 10 - drift * 22));\n\n  // 3. PROOF (0-10): specifics beat adjectives\n  const proofHits = (script.match(/\\d+(\\.\\d+)?\\s?(%|days?|weeks?|minutes?|x\\b|hours?)/gi) || []).length;\n  const proofScore = Math.min(10, 3 + proofHits * 2.2);\n\n  // 4. PAIN MATCH (0-10): does it name the pain point the brief asked for?\n  const painTokens = String(brief.pain_point || '').toLowerCase().split(/\\s+/).filter(w => w.length > 3);\n  const painHits   = painTokens.filter(t => blob.includes(t)).length;\n  const painScore  = painTokens.length ? Math.min(10, (painHits / painTokens.length) * 10) : 5;\n\n  // 5. NATIVE FEEL (0-10): first person, spoken cadence, not ad copy\n  let nativeScore = 3;\n  if (/\\b(i|my|me)\\b/i.test(script)) nativeScore += 3;\n  if (/\\b(honestly|literally|ok so|not gonna lie|i'm not)\\b/i.test(script)) nativeScore += 2;\n  if (script.split(/[.!?]/).filter(s => s.trim()).some(s => s.trim().split(/\\s+/).length > 28)) nativeScore -= 2;\n  nativeScore = Math.max(0, Math.min(10, nativeScore + 2));\n\n  // 6. SHOT LIST (0-10): enough distinct, shootable cuts for a 25-35s edit\n  const distinctShots = new Set(shots.map(s => String(s.shot || s).toLowerCase().trim())).size;\n  const shotScore = Math.max(0, Math.min(10, distinctShots * 1.8 - (distinctShots < 4 ? 3 : 0)));\n\n  // 7. CTA (0-10)\n  const ctaScore = cta ? Math.min(10, 5 + (/\\b(link in bio|shop|get yours|try|comment)\\b/i.test(cta) ? 4 : 0)\n                                   + (cta.split(/\\s+/).length <= 12 ? 1 : 0)) : 0;\n\n  const weights = { hook: 0.26, pacing: 0.12, proof: 0.14, pain: 0.16, native: 0.14, shots: 0.10, cta: 0.08 };\n  let total = (hookScore * weights.hook + pacingScore * weights.pacing + proofScore * weights.proof +\n               painScore * weights.pain + nativeScore * weights.native + shotScore * weights.shots +\n               ctaScore * weights.cta) * 10;\n\n  const violations = BANNED.filter(b => blob.includes(b));\n  total -= violations.length * 15;                 // compliance is a hard penalty\n  total = Math.round(Math.max(0, Math.min(100, total)) * 10) / 10;\n\n  const sub = { hook: hookScore, pacing: pacingScore, proof: proofScore, pain: painScore,\n                native: nativeScore, shots: shotScore, cta: ctaScore };\n  const weakest = Object.entries(sub).sort((a, b) => a[1] - b[1])[0];\n\n  out.push({ json: {\n    ...brief,\n    hook, script, cta,\n    shot_list: shots,\n    est_runtime_sec: Math.round(estRuntime),\n    word_count: scriptWords,\n    score_hook: Math.round(hookScore * 10) / 10,\n    score_pacing: Math.round(pacingScore * 10) / 10,\n    score_proof: Math.round(proofScore * 10) / 10,\n    score_pain_match: Math.round(painScore * 10) / 10,\n    score_native: Math.round(nativeScore * 10) / 10,\n    score_shotlist: Math.round(shotScore * 10) / 10,\n    score_cta: Math.round(ctaScore * 10) / 10,\n    total_score: total,\n    compliance_violations: violations.join(', '),\n    weakest_dimension: weakest[0],\n    decision: total >= APPROVE_AT && !violations.length ? 'approve' : 'reject',\n    reject_reason: violations.length ? 'compliance: ' + violations.join(', ')\n                 : total >= APPROVE_AT ? '' : 'scored ' + total + ' (need ' + APPROVE_AT + '), weakest = ' + weakest[0]\n  } });\n}\nreturn out;"}},{"id":"aa11bb22-0007-4c33-8d44-ee55ff660000","name":"Score Above 78?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1600,300],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"c1","leftValue":"={{ $json.total_score }}","rightValue":78,"operator":{"type":"number","operation":"gte"}},{"id":"c2","leftValue":"={{ $json.compliance_violations }}","rightValue":"","operator":{"type":"string","operation":"equals"}}]},"options":{}}},{"id":"aa11bb22-0008-4c33-8d44-ee55ff660000","name":"Shape Production Ticket","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[1860,60],"parameters":{"assignments":{"assignments":[{"id":"a1","name":"angle_key","value":"={{ $json.angle_key }}","type":"string"},{"id":"a2","name":"product","value":"={{ $json.product_name }}","type":"string"},{"id":"a3","name":"pain_point","value":"={{ $json.pain_point }}","type":"string"},{"id":"a4","name":"creator_persona","value":"={{ $json.creator_persona }}","type":"string"},{"id":"a5","name":"hook","value":"={{ $json.hook }}","type":"string"},{"id":"a6","name":"script","value":"={{ $json.script }}","type":"string"},{"id":"a7","name":"shot_list","value":"={{ JSON.stringify($json.shot_list) }}","type":"string"},{"id":"a8","name":"total_score","value":"={{ $json.total_score }}","type":"number"},{"id":"a9","name":"est_runtime_sec","value":"={{ $json.est_runtime_sec }}","type":"number"},{"id":"a10","name":"status","value":"queued_for_creator","type":"string"}]},"options":{}}},{"id":"aa11bb22-0009-4c33-8d44-ee55ff660000","name":"Queue Ticket In Airtable","type":"n8n-nodes-base.airtable","typeVersion":2.1,"position":[2120,60],"parameters":{"operation":"create","base":{"__rl":true,"value":"appUGCFACTORY","mode":"id"},"table":{"__rl":true,"value":"tblScriptQueue","mode":"id"},"columns":{"mappingMode":"autoMapInputData","value":{}},"options":{}}},{"id":"aa11bb22-0010-4c33-8d44-ee55ff660000","name":"Ping Creator Channel","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[2380,60],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#ugc-production","mode":"name"},"text":"=🎬 New UGC script queued — *{{ $json.product }}* ({{ $json.total_score }}/100)\nPersona: {{ $json.creator_persona }} · ~{{ $json.est_runtime_sec }}s\nHook: _{{ $json.hook }}_","otherOptions":{}}},{"id":"aa11bb22-0011-4c33-8d44-ee55ff660000","name":"Diagnose Weak Scripts","type":"n8n-nodes-base.code","typeVersion":2,"position":[1860,540],"parameters":{"jsCode":"// ---- REJECT TRIAGE --------------------------------------------------------\n// Turn a failed script into an actionable rewrite brief so the next pass is better.\nconst FIXES = {\n  hook:      'Rewrite the first 3 seconds as a pattern interrupt: a number, a negation or a question. Max 12 words, no greeting.',\n  pacing:    'Cut or expand the script to hit the target runtime at 2.6 words/second.',\n  proof:     'Add two concrete specifics (a number, a timeframe, a measurable change) instead of adjectives.',\n  pain:      'Name the exact pain point from the brief in the creator\\'s own words within the first 5 seconds.',\n  native:    'Rewrite in first person spoken cadence. Short sentences, no marketing voice.',\n  shots:     'Expand the shot list to at least 6 distinct, shootable cuts with a b-roll for every claim.',\n  cta:       'Add a single short CTA naming the action and where to take it.'\n};\nreturn $input.all().map(i => {\n  const r = i.json;\n  const gap = Math.max(0, 78 - (Number(r.total_score) || 0));\n  return { json: {\n    ...r,\n    score_gap: Math.round(gap * 10) / 10,\n    retry_recommended: !r.compliance_violations && gap <= 12,\n    rewrite_instruction: r.compliance_violations\n      ? 'Strip the non-compliant claims (' + r.compliance_violations + ') and re-submit.'\n      : (FIXES[r.weakest_dimension] || 'Rewrite against the rubric.'),\n    logged_at: new Date().toISOString()\n  } };\n});"}},{"id":"aa11bb22-0012-4c33-8d44-ee55ff660000","name":"Log Rejects For Rewrite","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[2120,540],"parameters":{"operation":"append","documentId":{"__rl":true,"value":"UGC_FACTORY_SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"gid=112233","mode":"list","cachedResultName":"Rejected Scripts"},"columns":{"mappingMode":"autoMapInputData","value":{},"matchingColumns":[]},"options":{}}},{"id":"aa11bb22-0013-4c33-8d44-ee55ff660000","name":"Summarise Run And Pacing","type":"n8n-nodes-base.code","typeVersion":2,"position":[1340,760],"parameters":{"jsCode":"// ---- RUN SUMMARY + PACING PROJECTION -------------------------------------\n// Runs once after the batch loop finishes. Reads everything the scorer produced.\nconst MONTHLY_TARGET = 400;    // approved UGC scripts we owe the creators each month\nconst all = $('Score Scripts Against Rubric').all().map(i => i.json);\nconst approved = all.filter(r => r.decision === 'approve');\nconst rejected = all.filter(r => r.decision !== 'approve');\n\nconst avg = (xs) => xs.length ? Math.round((xs.reduce((s, x) => s + x, 0) / xs.length) * 10) / 10 : 0;\nconst approvalRate = all.length ? approved.length / all.length : 0;\n\n// which rubric dimension is costing us the most volume?\nconst failCounts = {};\nrejected.forEach(r => { failCounts[r.weakest_dimension] = (failCounts[r.weakest_dimension] || 0) + 1; });\nconst topFailure = Object.entries(failCounts).sort((a, b) => b[1] - a[1])[0] || ['none', 0];\n\n// pacing: at today's approved throughput, where do we land this month?\nconst now = new Date();\nconst daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();\nconst projectedMonthly = approved.length * daysInMonth;\nconst pacingPct = Math.round((projectedMonthly / MONTHLY_TARGET) * 100);\nconst dailyNeeded = Math.ceil(MONTHLY_TARGET / daysInMonth);\n\nconst byProduct = {};\napproved.forEach(r => { byProduct[r.product_name] = (byProduct[r.product_name] || 0) + 1; });\n\nreturn [{ json: {\n  run_date: now.toISOString().slice(0, 10),\n  generated: all.length,\n  approved: approved.length,\n  rejected: rejected.length,\n  approval_rate_pct: Math.round(approvalRate * 1000) / 10,\n  avg_score_all: avg(all.map(r => Number(r.total_score) || 0)),\n  avg_score_approved: avg(approved.map(r => Number(r.total_score) || 0)),\n  avg_hook_score: avg(all.map(r => Number(r.score_hook) || 0)),\n  top_failure_dimension: topFailure[0],\n  top_failure_count: topFailure[1],\n  compliance_flags: all.filter(r => r.compliance_violations).length,\n  daily_needed: dailyNeeded,\n  projected_monthly_approved: projectedMonthly,\n  pacing_pct_of_target: pacingPct,\n  on_pace: approved.length >= dailyNeeded,\n  approved_by_product: JSON.stringify(byProduct),\n  html: '<h3>UGC factory ' + now.toISOString().slice(0, 10) + '</h3>' +\n        '<p>' + approved.length + ' approved / ' + all.length + ' generated (' +\n        Math.round(approvalRate * 100) + '% approval, avg score ' + avg(all.map(r => Number(r.total_score) || 0)) + ').</p>' +\n        '<p>Projected month: ' + projectedMonthly + ' of ' + MONTHLY_TARGET + ' (' + pacingPct + '% of target). ' +\n        'Biggest rubric failure: <b>' + topFailure[0] + '</b> (' + topFailure[1] + ' scripts).</p>'\n} }];"}},{"id":"aa11bb22-0014-4c33-8d44-ee55ff660000","name":"On Pace For Monthly Target?","type":"n8n-nodes-base.if","typeVersion":2,"position":[1600,760],"parameters":{"conditions":{"options":{"caseSensitive":true,"version":2},"combinator":"and","conditions":[{"id":"p1","leftValue":"={{ $json.approved }}","rightValue":"={{ $json.daily_needed }}","operator":{"type":"number","operation":"gte"}}]},"options":{}}},{"id":"aa11bb22-0015-4c33-8d44-ee55ff660000","name":"Email Daily Factory Digest","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[1860,680],"parameters":{"sendTo":"creative@brand.com","subject":"=UGC factory: {{ $json.approved }} scripts queued {{ $now.format('yyyy-LL-dd') }}","message":"={{ $json.html }}","options":{}}},{"id":"aa11bb22-0016-4c33-8d44-ee55ff660000","name":"Alert Volume Shortfall","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[1860,860],"parameters":{"select":"channel","channelId":{"__rl":true,"value":"#ugc-production","mode":"name"},"text":"=⚠️ Behind pace: only {{ $json.approved }} approved today vs {{ $json.daily_needed }} needed. Projecting {{ $json.projected_monthly_approved }} this month ({{ $json.pacing_pct_of_target }}% of target). Biggest rubric failure: {{ $json.top_failure_dimension }} ({{ $json.top_failure_count }} scripts).","otherOptions":{}}},{"id":"aa11bb22-0017-4c33-8d44-ee55ff660000","name":"Section: Concept Sourcing","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[20,40],"parameters":{"content":"## 1. CONCEPT SOURCING\nPull the product backlog, score each SKU on proven ROAS + margin minus creative saturation, allocate the daily quota of 24 concepts proportionally, then expand product × pain point × format and dedupe against angles already produced.","height":240,"width":700,"color":4}},{"id":"aa11bb22-0018-4c33-8d44-ee55ff660000","name":"Section: Generate And Score","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1060,40],"parameters":{"content":"## 2. GENERATE + SCORE\nBatches of 4 go to the LLM for hook / script / shot list, then the rubric scorer grades 7 weighted dimensions (hook 26%, pain match 16%, proof 14%, native feel 14%, pacing 12%, shot list 10%, CTA 8%) with a -15 hit per banned claim. Approve at 78.","height":240,"width":700,"color":3}},{"id":"aa11bb22-0019-4c33-8d44-ee55ff660000","name":"Section: Queue And Pacing","type":"n8n-nodes-base.stickyNote","typeVersion":1,"position":[1840,1000],"parameters":{"content":"## 3. QUEUE + PACING\nWinners become Airtable production tickets and ping the creator channel. Losers get a diagnosed rewrite instruction and land in the rewrite sheet. When the batch loop finishes, the run is projected against the 400/month target.","height":220,"width":700,"color":5}}],"connections":{"Daily Factory Trigger":{"main":[[{"node":"Load Product Backlog","type":"main","index":0}]]},"Load Product Backlog":{"main":[[{"node":"Build Concept Briefs","type":"main","index":0}]]},"Build Concept Briefs":{"main":[[{"node":"Batch Briefs","type":"main","index":0}]]},"Batch Briefs":{"main":[[{"node":"Summarise Run And Pacing","type":"main","index":0}],[{"node":"Generate Hook Script Shotlist","type":"main","index":0}]]},"Generate Hook Script Shotlist":{"main":[[{"node":"Score Scripts Against Rubric","type":"main","index":0}]]},"Score Scripts Against Rubric":{"main":[[{"node":"Score Above 78?","type":"main","index":0}]]},"Score Above 78?":{"main":[[{"node":"Shape Production Ticket","type":"main","index":0}],[{"node":"Diagnose Weak Scripts","type":"main","index":0}]]},"Shape Production Ticket":{"main":[[{"node":"Queue Ticket In Airtable","type":"main","index":0}]]},"Queue Ticket In Airtable":{"main":[[{"node":"Ping Creator Channel","type":"main","index":0}]]},"Ping Creator Channel":{"main":[[{"node":"Batch Briefs","type":"main","index":0}]]},"Diagnose Weak Scripts":{"main":[[{"node":"Log Rejects For Rewrite","type":"main","index":0}]]},"Log Rejects For Rewrite":{"main":[[{"node":"Batch Briefs","type":"main","index":0}]]},"Summarise Run And Pacing":{"main":[[{"node":"On Pace For Monthly Target?","type":"main","index":0}]]},"On Pace For Monthly Target?":{"main":[[{"node":"Email Daily Factory Digest","type":"main","index":0}],[{"node":"Alert Volume Shortfall","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"},"pinData":{}}