This workflow follows the Execute Workflow Trigger → Google Sheets 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 →
{
"name": "14 - Lead Scout Triage & Scoring (Deterministic, Stage 3.5)",
"nodes": [
{
"parameters": {
"content": "## WF14 \u2014 Lead Scout Triage & Scoring v0.3 (Stage 3.5, DEC-138)\n\n**\u041d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435:** \u0446\u0435\u043d\u0442\u0440\u0430\u043b\u044c\u043d\u044b\u0439 \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 Lead Scout. $0, \u0411\u0415\u0417 Claude, \u0411\u0415\u0417 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0432\u044b\u0437\u043e\u0432\u043e\u0432. \u0427\u0438\u0442\u0430\u0435\u0442 raw_market_records (audience-\u0441\u0442\u0440\u043e\u043a\u0438 \u2014 \u041f\u0415\u0420\u0412\u0418\u0427\u041d\u041e, \u043d\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 WF08) + review_queue (\u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435), \u0441\u043a\u043e\u0440\u0438\u0442 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u044b (0-100), \u0438\u0437\u0432\u043b\u0435\u043a\u0430\u0435\u0442 \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430 \u0438 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 \u0432\u0441\u0451 \u043d\u0430 \u0420\u0423\u0427\u041d\u0423\u042e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c. \u041f\u0438\u0448\u0435\u0442 public_lead_signals (v0.3, 47 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, docs/TABLE_SCHEMA.md \u00a7G) + agent_requests.\n\n**\u0421\u043a\u043e\u0440\u0438\u043d\u0433 (\u0434\u0435\u0442\u0435\u0440\u043c.):** intent(25)+urgency(15)+pain(20)+niche(15)+contact(8)+region(7)+freshness(10) - \u0448\u0442\u0440\u0430\u0444\u044b. \u041f\u043e\u043b\u043e\u0441\u044b: >=75 high / 50-74 medium / 25-49 low / <25 ignore (\u043d\u0435 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u043f\u0440\u0438 min_lead_score=25). \u041f\u043e\u043b\u044f: lead_score, score_band, score_reasons, review_priority, recommended_action.\n\n**pain_type:** bank_refusal \u00b7 bad_credit_history \u00b7 overdue_debt \u00b7 refinancing_need \u00b7 mortgage_need \u00b7 business_finance_need \u00b7 pledge_auto_pts \u00b7 broker_price_question \u00b7 fraud_fear \u00b7 prepayment_fear \u00b7 document_problem \u00b7 unknown.\n**intent_type:** question \u00b7 objection \u00b7 complaint \u00b7 buying_intent \u00b7 content_signal.\n\n**\u0416\u0451\u0441\u0442\u043a\u0438\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 (CONTACT_AND_OUTREACH_POLICY):** \u0442\u043e\u043b\u044c\u043a\u043e \u041f\u0423\u0411\u041b\u0418\u0427\u041d\u042b\u0415 \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430; \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c/\u043a\u043e\u043d\u0442\u0430\u043a\u0442 = evidence, \u041d\u0415 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0430 \u0430\u0443\u0442\u0440\u0438\u0447; outreach_allowed=false \u0412\u0421\u0415\u0413\u0414\u0410; recommended_action \u2208 {manual_review, content_idea, monitor, ignore}; \u043a\u043e\u043d\u0442\u0430\u043a\u0442 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432 \u0442\u0435\u043a\u0441\u0442\u0435 + \u0435\u0441\u0442\u044c contact_source_url, \u0438\u043d\u0430\u0447\u0435 \u0431\u043b\u0430\u043d\u043a + privacy_flag; \u043d\u0435\u0442 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0445 \u0447\u0430\u0442\u043e\u0432 / \u0441\u043a\u0440\u044b\u0442\u044b\u0445 / \u0432\u044b\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0445 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432 / \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0438 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432. review_status: new \u2192 needs_review \u2192 accepted/rejected/duplicate/stale. \u0414\u0435\u0434\u0443\u043f: lead_signal_id (\u0445\u044d\u0448) + comment_url/post_url+texthash/profile_url+texthash/platform+texthash. \u041a\u0432\u043e\u0442\u0430-safe (DEC-131): \u043a\u0430\u0436\u0434\u0430\u044f \u0432\u043a\u043b\u0430\u0434\u043a\u0430 \u0447\u0438\u0442\u0430\u0435\u0442\u0441\u044f 1 \u0440\u0430\u0437, \u043f\u0443\u043b \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d, append \u226425. active=false; \u0440\u0443\u0447\u043d\u043e\u0439 \u0437\u0430\u043f\u0443\u0441\u043a; MSK +03:00.",
"height": 640,
"width": 660
},
"id": "wf14-note-1",
"name": "Overview Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-480,
-360
]
},
{
"parameters": {
"content": "## Setup & Test (v0.3 \u2014 Stage 3.5)\n\n1. \u0412\u043a\u043b\u0430\u0434\u043a\u0430 public_lead_signals = 47 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3 (docs/TABLE_SCHEMA.md \u00a7G). \u041c\u0438\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u0440\u0443\u044e 28-\u043a\u043e\u043b\u043e\u043d\u043e\u0447\u043d\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443 \u043f\u043e docs/GOOGLE_SHEETS_VALIDATION_PLAN. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c.\n2. \u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c SPREADSHEET_ID \u0438 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u043d\u0430 5 Google Sheets \u043d\u043e\u0434\u0430\u0445 (3 read + 2 append).\n3. \u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u2014 \u041d\u0415 \u043f\u043e \u043a\u0430\u0436\u0434\u043e\u0439 \u043d\u043e\u0434\u0435. \u041f\u043e\u043b\u043d\u0430\u044f \u043f\u0440\u0438\u0451\u043c\u043a\u0430 Stage 3.5 = \u043f\u0430\u043a\u0435\u0442 Stage C (\u0441\u043c. docs/STAGE_C_ACCEPTANCE_PACK.md). \u0411\u044b\u0441\u0442\u0440\u044b\u0439 smoke ($0): \u043f\u043e\u0441\u043b\u0435 WF13 fixture \u2192 WF14 Execute once \u2192 public_lead_signals: high-intent \u0441\u0442\u0440\u043e\u043a\u0438 (after_refusal/buying_intent) \u0441 lead_score, score_band, review_priority; \u043a\u043e\u043d\u0442\u0430\u043a\u0442-fixture \u0441 \u0441\u0438\u043d\u0442\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043e\u043c \u2192 public_phone \u043a\u0430\u043a \u0442\u0435\u043a\u0441\u0442 (\u0431\u0435\u0437 #ERROR!) + contact_use_policy=manual_review; \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0439/\u043d\u043e\u0432\u043e\u0441\u0442\u043d\u043e\u0439 fixture \u041d\u0415 \u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u043b\u0438\u0434\u043e\u043c; outreach_allowed=false \u0432\u043e \u0432\u0441\u0435\u0445 \u0441\u0442\u0440\u043e\u043a\u0430\u0445; Final Summary self_test_passed=true.\n4. \u041f\u043e\u0432\u0442\u043e\u0440: signals_written=0, duplicates_skipped>0, status=completed_no_data.\n5. \u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c self-test: read_once_ok / append_cap_ok / dedup_ok / policy_ok = true.",
"height": 640,
"width": 620
},
"id": "wf14-note-2",
"name": "Setup and Test Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
220,
-360
]
},
{
"parameters": {},
"id": "wf14-start",
"name": "Manual Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-420,
560
]
},
{
"parameters": {
"jsCode": "// WF14 v0.3 \u2014 Lead Scout Triage & Scoring config (Stage 3.5, DEC-138). Deterministic, $0, NO Claude, NO external calls.\n// Central lead engine: reads raw_market_records audience rows (PRIMARY) + review_queue (enrichment), scores public\n// lead signals, extracts ONLY public contact evidence, routes everything to MANUAL review. Quota-safe (DEC-131):\n// each tab read exactly once, bounded candidate pool, capped batched append. Public evidence only; a public\n// profile/contact is EVIDENCE, never outreach permission; outreach_allowed=false ALWAYS (CONTACT_AND_OUTREACH_POLICY).\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){var z=function(n){return String(n).padStart(2,'0');};var m=new Date(Date.now()+10800000);return m.getUTCFullYear()+z(m.getUTCMonth()+1)+z(m.getUTCDate())+'_'+z(m.getUTCHours())+z(m.getUTCMinutes())+z(m.getUTCSeconds());}\nconst stamp=moscowStamp();\nvar __cfg = {\n // ---- schema / scope (quota safety; DEC-131)\n schema_version:'public_lead_signals_v0.3',\n time_window_days:30,\n max_source_rows:120, // working candidate pool cap (applied AFTER score/sort/filter)\n max_signals_to_write:25, // HARD ceiling on appended rows\n platform_filter:'', // '' = any; operator may set 'vk'\n source_type_filter:'', // '' = any\n only_untriaged:true,\n include_review_queue:true, // clean-isolation: set false so old review_queue rows can't contaminate a WF13-run acceptance test\n source_agent_request_id:'', // '' = all sources; set to a WF13 run's agent_request_id to triage ONLY that source run (Defect G)\n backfill_untriaged:true, // allow older untriaged high-score rows when the fresh window is thin\n // ---- scoring engine (deterministic; weights sum to 100 before penalties)\n min_lead_score:25, // below this -> not written (ignore band kept out of the sheet by default)\n band_high:75, // >=75 high priority manual review\n band_medium:50, // 50-74 medium\n band_low:25, // 25-49 low (monitor / content idea)\n score_weights:{ intent:25, urgency:15, pain:20, niche:15, contact:8, region:7, freshness:10 },\n // ---- contact / outreach policy (CONTACT_AND_OUTREACH_POLICY: evidence, NOT permission)\n contact_use_policy_default:'aggregate_only',\n contact_use_policy_with_public_contact:'manual_review',\n contact_use_policy_weak_evidence:'do_not_use',\n outreach_allowed:false, // ALWAYS false in Stage 3.5 \u2014 no auto-outreach, ever\n allowed_recommended_actions:['manual_review','content_idea','monitor','ignore'],\n forbidden_recommended_actions:['outreach','contact','dm','message','call','email','send'],\n region_default:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n requested_by:'operator',\n // ---- deterministic ids + MSK stamps\n run_stamp:stamp,\n run_id:'wf14_'+stamp,\n agent_request_id:'wf14_req_'+stamp,\n created_at:moscowIsoNow()\n};\n// D2 (VK-COMMENTS-001): request-scoping via the callable trigger. Manual runs (empty inputs) keep every default.\nvar __in={}; try{ __in=$('When Called by Agent').first().json||{}; }catch(e){ __in=(typeof $json!=='undefined'&&$json&&typeof $json==='object')?$json:{}; }\nfunction __num(v){var n=Number(v);return isNaN(n)?null:n;}\nfunction __bool(v){if(v===true||v===false)return v;var s=String(v==null?'':v).toLowerCase();if(s==='true')return true;if(s==='false')return false;return null;}\nif(String(__in.source_agent_request_id||'')!=='')__cfg.source_agent_request_id=String(__in.source_agent_request_id);\nif(String(__in.agent_request_id||'')!=='')__cfg.agent_request_id=String(__in.agent_request_id);\nif(String(__in.platform_filter||'')!=='')__cfg.platform_filter=String(__in.platform_filter);\nif(String(__in.source_type_filter||'')!=='')__cfg.source_type_filter=String(__in.source_type_filter);\nif(__num(__in.min_lead_score)!=null)__cfg.min_lead_score=__num(__in.min_lead_score);\nif(__bool(__in.include_review_queue)!=null)__cfg.include_review_queue=__bool(__in.include_review_queue);\nreturn [{ json: __cfg }];\n"
},
"id": "wf14-config",
"name": "Set Triage Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-200,
560
]
},
{
"id": "wf14-read-review",
"name": "Read review_queue",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
20,
560
],
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "review_queue",
"mode": "name"
},
"options": {}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"jsCode": "// Collapse to a single control item so the NEXT Google Sheets read executes EXACTLY ONCE.\n// Prevents item explosion (a multi-item read feeding another read = read-per-item = quota error).\n// Sheet data stays available downstream via $('Read ...').all().\nreturn [{ json: $('Set Triage Config').first().json }];"
},
"id": "wf14-hold-a",
"name": "Hold Config (before raw)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
200,
560
]
},
{
"id": "wf14-read-raw",
"name": "Read raw_market_records",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
380,
560
],
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "raw_market_records",
"mode": "name"
},
"options": {}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"jsCode": "// Collapse to a single control item so the NEXT Google Sheets read executes EXACTLY ONCE.\n// Prevents item explosion (a multi-item read feeding another read = read-per-item = quota error).\n// Sheet data stays available downstream via $('Read ...').all().\nreturn [{ json: $('Set Triage Config').first().json }];"
},
"id": "wf14-hold-b",
"name": "Hold Config (before pls)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
560,
560
]
},
{
"id": "wf14-read-pls",
"name": "Read public_lead_signals",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
740,
560
],
"parameters": {
"authentication": "serviceAccount",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "public_lead_signals",
"mode": "name"
},
"options": {}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"jsCode": "// WF14 v0.3 \u2014 LEAD SCOUT TRIAGE & SCORING ENGINE (deterministic, $0, no Claude; Stage 3.5, DEC-138).\n// Reads raw_market_records audience rows (PRIMARY) + review_queue (enrichment) ONCE each (upstream collapse nodes\n// guarantee single execution -> quota-safe, DEC-131). Classifies pain/intent, scores demand deterministically,\n// extracts ONLY public contact evidence, and routes everything to MANUAL review.\n// HARD RULES (CONTACT_AND_OUTREACH_POLICY): public evidence only; a public profile/contact is EVIDENCE, never\n// outreach permission; recommended_action constrained to {manual_review, content_idea, monitor, ignore} (outreach\n// impossible by construction); outreach_allowed=false ALWAYS; no hidden/inferred contacts; no member extraction.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cut(s,n){s=str(s);return s.length>n?(s.slice(0,n-1)+'\u2026'):s;}\nfunction norm(s){return low(s).replace(/\\s+/g,' ').trim().slice(0,240);}\nfunction h32(s,seed){s=str(s);let h=seed>>>0;for(let i=0;i<s.length;i++){h=((h<<5)+h+s.charCodeAt(i))>>>0;}return h>>>0;}\nfunction hex(h){return ('0000000'+(h>>>0).toString(16)).slice(-8);}\nfunction sheetsSafe(v){v=str(v);return /^[+=]/.test(v)?(\"'\"+v):v;}\nfunction rows(name){try{return $(name).all().map(i=>i.json).filter(r=>r&&Object.keys(r).some(k=>str(r[k])!==''));}catch(e){return [];}}\n\nconst cfg=$('Set Triage Config').first().json;\nconst W=cfg.score_weights||{intent:25,urgency:15,pain:20,niche:15,contact:8,region:7,freshness:10};\nconst windowDays=Number(cfg.time_window_days)||30;\nconst cutoffMs=Date.now()-windowDays*86400000;\nconst maxSource=Math.max(1,Math.min(200,Number(cfg.max_source_rows)||120));\nconst maxWrite=Math.max(1,Math.min(25,Number(cfg.max_signals_to_write)||25)); // hard ceiling 25\nconst minScore=Number(cfg.min_lead_score)||0;\nconst bandHigh=Number(cfg.band_high)||75, bandMed=Number(cfg.band_medium)||50, bandLow=Number(cfg.band_low)||25;\nconst platformFilter=low(cfg.platform_filter);\nconst sourceTypeFilter=low(cfg.source_type_filter);\nconst backfill=cfg.backfill_untriaged!==false;\nconst ALLOWED=Array.isArray(cfg.allowed_recommended_actions)?cfg.allowed_recommended_actions:['manual_review','content_idea','monitor','ignore'];\nconst FORBIDDEN=['outreach','contact','dm','message','call','email','send'];\n\nconst includeReview=cfg.include_review_queue!==false;\nconst sourceReqId=low(cfg.source_agent_request_id);\nconst review=includeReview?rows('Read review_queue'):[];\nlet raw=rows('Read raw_market_records');\nif(sourceReqId){raw=raw.filter(r=>low(r.agent_request_id)===sourceReqId);}\nconst existing=rows('Read public_lead_signals');\n\n// ---- existing dedup (tolerate old 28-col names + new v0.3 names)\nfunction exPlatform(r){return low(r.source_platform||r.platform);}\nfunction exPost(r){return low(r.source_post_url||r.post_url);}\nfunction exCmt(r){return low(r.source_comment_url);}\nfunction exText(r){return norm(r.evidence_text||r.text_evidence);}\nfunction exProf(r){return low(r.source_public_profile_url||r.public_profile_url||r.profile_url);}\nconst existIds=new Set(existing.map(r=>low(r.lead_signal_id)).filter(Boolean));\nconst existKeyToId={};\nfunction addExKey(k,id){if(k&&!existKeyToId[k])existKeyToId[k]=id;}\nfor(const r of existing){\n const id=low(r.lead_signal_id), p=exPlatform(r), post=exPost(r), cmt=exCmt(r), prof=exProf(r), th=hex(h32(exText(r),5381)), th2=hex(h32(exText(r),7919));\n if(cmt)addExKey('c|'+p+'|'+cmt,id);\n if(post)addExKey('p|'+p+'|'+post+'|'+th,id);\n if(prof)addExKey('u|'+p+'|'+prof+'|'+th,id);\n addExKey('h|'+p+'|'+th2,id);\n}\n\n// ---- vocabularies (credit_brokerage v0.3; canonical pain set per Stage 3.5 brief)\nconst INTENT_HIGH=['\u043d\u0443\u0436\u0435\u043d \u043a\u0440\u0435\u0434\u0438\u0442','\u043d\u0443\u0436\u0435\u043d \u0437\u0430\u0439\u043c','\u043d\u0443\u0436\u043d\u0430 \u0438\u043f\u043e\u0442\u0435\u043a','\u043d\u0443\u0436\u043d\u043e \u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441','\u043d\u0443\u0436\u043d\u044b \u0434\u0435\u043d\u044c\u0433\u0438','\u0438\u0449\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0430','\u0438\u0449\u0443 \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430','\u043f\u043e\u043c\u043e\u0433\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u043f\u043e\u043c\u043e\u0433\u0438\u0442\u0435 \u0432\u0437\u044f\u0442\u044c','\u0445\u043e\u0447\u0443 \u0432\u0437\u044f\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442','\u0433\u0434\u0435 \u0432\u0437\u044f\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043a\u0442\u043e \u043f\u043e\u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442 \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','\u043d\u0443\u0436\u0435\u043d \u0437\u0430\u043b\u043e\u0433\u043e\u0432\u044b\u0439','\u0434\u0435\u043d\u044c\u0433\u0438 \u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433','\u043d\u0443\u0436\u043d\u0430 \u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0430\u044f \u0433\u0430\u0440\u0430\u043d\u0442\u0438','\u043d\u0443\u0436\u0435\u043d \u0442\u0435\u043d\u0434\u0435\u0440\u043d\u044b\u0439'];\nconst INTENT_MED=['\u043f\u043e\u0447\u0435\u043c\u0443 \u043e\u0442\u043a\u0430\u0437','\u043a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0438','\u043a\u0430\u043a \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u0433\u0434\u0435 \u043b\u0443\u0447\u0448\u0435 \u0432\u0437\u044f\u0442\u044c','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','\u043a \u043a\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f','\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435','\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435','\u043a\u0442\u043e \u0431\u0440\u0430\u043b','\u043a\u0442\u043e \u043e\u0431\u0440\u0430\u0449\u0430\u043b\u0441\u044f','\u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c','\u043d\u0443\u0436\u0435\u043d \u0441\u043e\u0432\u0435\u0442'];\nconst QUESTION=['\u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0438\u0442\u0435','\u043f\u043e\u0441\u043e\u0432\u0435\u0442\u0443\u0439\u0442\u0435','\u043a\u0442\u043e \u0431\u0440\u0430\u043b','\u043a\u0442\u043e \u043e\u0431\u0440\u0430\u0449\u0430\u043b\u0441\u044f','\u043a \u043a\u043e\u043c\u0443 \u043e\u0431\u0440\u0430\u0449\u0430\u0442\u044c\u0441\u044f','\u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u0447\u0442\u043e \u0434\u0435\u043b\u0430\u0442\u044c','\u043d\u0443\u0436\u0435\u043d \u0441\u043e\u0432\u0435\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','?'];\nconst OBJECTION=['\u043d\u0435 \u0432\u0435\u0440\u044e','\u0431\u043e\u044e\u0441\u044c','\u0440\u0430\u0437\u0432\u043e\u0434','\u043e\u0431\u043c\u0430\u043d','\u043a\u0438\u0434\u0430\u043b','\u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442'];\nconst COMPLAINT=['\u0436\u0430\u043b\u043e\u0431','\u043e\u0431\u043c\u0430\u043d\u0443\u043b\u0438','\u043a\u0438\u043d\u0443\u043b\u0438','\u043d\u0435 \u0432\u0435\u0440\u043d\u0443\u043b\u0438','\u043e\u0442\u0432\u0440\u0430\u0442\u0438\u0442\u0435\u043b\u044c\u043d','\u0437\u0430\u0431\u0440\u0430\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0438','\u0432\u0437\u044f\u043b\u0438 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438'];\nconst URGENT_HIGH=['\u0441\u0440\u043e\u0447\u043d\u043e','\u0441\u0435\u0433\u043e\u0434\u043d\u044f','\u0434\u043e \u0437\u0430\u0432\u0442\u0440\u0430','\u0433\u043e\u0440\u0438\u0442','\u0433\u043e\u0440\u0438\u0442 \u0441\u0434\u0435\u043b\u043a\u0430','\u043e\u0447\u0435\u043d\u044c \u0441\u0440\u043e\u0447\u043d\u043e','\u0434\u0435\u043d\u044c\u0433\u0438 \u043d\u0443\u0436\u043d\u044b \u0441\u0440\u043e\u0447\u043d\u043e'];\nconst URGENT_MED=['\u0432 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0435','\u043f\u043b\u0430\u043d\u0438\u0440\u0443\u044e','\u043d\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u043d\u0435\u0434\u0435\u043b\u0435','\u0441\u043a\u043e\u0440\u043e \u043f\u043e\u043d\u0430\u0434\u043e\u0431','\u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043c\u0435\u0441\u044f\u0446'];\nconst PAIN=[\n ['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0438','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437 \u0431\u0430\u043d\u043a','bank_refusal'],['\u0431\u0430\u043d\u043a \u043e\u0442\u043a\u0430\u0437','bank_refusal'],['\u043e\u0442\u043a\u0430\u0437\u043e\u0432 \u0431\u0430\u043d\u043a','bank_refusal'],['\u0431\u0430\u043d\u043a\u0438 \u043e\u0442\u043a\u0430\u0437','bank_refusal'],\n ['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0438','bad_credit_history'],['\u0438\u0441\u043f\u043e\u0440\u0447\u0435\u043d\u0430 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','bad_credit_history'],['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438','bad_credit_history'],\n ['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','overdue_debt'],['\u0437\u0430\u0434\u043e\u043b\u0436\u0435\u043d\u043d\u043e\u0441\u0442','overdue_debt'],['\u0434\u043e\u043b\u0433 \u043f\u043e \u043a\u0440\u0435\u0434\u0438\u0442','overdue_debt'],\n ['\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440','refinancing_need'],['\u0441\u043d\u0438\u0437\u0438\u0442\u044c \u0441\u0442\u0430\u0432\u043a','refinancing_need'],['\u043f\u0435\u0440\u0435\u043a\u0440\u0435\u0434\u0438\u0442','refinancing_need'],\n ['\u0438\u043f\u043e\u0442\u0435\u043a','mortgage_need'],\n ['\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430','business_finance_need'],['\u0434\u043b\u044f \u0438\u043f','business_finance_need'],['\u0434\u043b\u044f \u043e\u043e\u043e','business_finance_need'],['\u043e\u0431\u043e\u0440\u043e\u0442\u043d','business_finance_need'],['\u0442\u0435\u043d\u0434\u0435\u0440\u043d','business_finance_need'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a \u0433\u0430\u0440\u0430\u043d\u0442\u0438','business_finance_need'],\n ['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u043f\u0442\u0441','pledge_auto_pts'],['\u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e','pledge_auto_pts'],['\u043f\u043e\u0434 \u043f\u0442\u0441','pledge_auto_pts'],['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433 \u043d\u0435\u0434\u0432\u0438\u0436','pledge_auto_pts'],['\u0437\u0430\u043b\u043e\u0433 \u043d\u0435\u0434\u0432\u0438\u0436','pledge_auto_pts'],['\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433','pledge_auto_pts'],\n ['\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u043e \u0441\u0442\u043e\u0438\u0442','broker_price_question'],['\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u043e\u0438\u0442','broker_price_question'],['\u043a\u0430\u043a\u0430\u044f \u043a\u043e\u043c\u0438\u0441\u0441\u0438','broker_price_question'],['\u043f\u0440\u043e\u0446\u0435\u043d\u0442 \u0431\u0440\u043e\u043a\u0435\u0440','broker_price_question'],['\u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','broker_price_question'],\n ['\u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','fraud_fear'],['\u043e\u0431\u043c\u0430\u043d','fraud_fear'],['\u043a\u0438\u0434\u0430\u043b','fraud_fear'],['\u0440\u0430\u0437\u0432\u043e\u0434','fraud_fear'],\n ['\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442','prepayment_fear'],\n ['\u0441\u043f\u0440\u0430\u0432\u043a','document_problem'],['2-\u043d\u0434\u0444\u043b','document_problem'],['\u0431\u0435\u0437 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434','document_problem'],['\u043d\u0435\u0442 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442','document_problem']\n];\nconst SEVERE=['bank_refusal','overdue_debt','bad_credit_history','pledge_auto_pts'];\n// supplier/ad markers: a BROKER advertising is NOT a consumer lead -> excluded from the lead pool\nconst SUPPLIER=['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\u0438\u043f\u043e\u0442\u0435\u0447\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\u043f\u043e\u043c\u043e\u0449\u044c \u0432 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u043a\u0440\u0435\u0434\u0438\u0442\u0430','\u043f\u043e\u043c\u043e\u0433\u0430\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u0430','\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a\u0430','\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0437\u0430\u044f\u0432\u043a\u0443','\u0440\u0430\u0431\u043e\u0442\u0430\u0435\u043c \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u043d\u0430\u0448\u0438 \u0443\u0441\u043b\u0443\u0433\u0438','\u0437\u0432\u043e\u043d\u0438\u0442\u0435','\u043f\u0438\u0448\u0438\u0442\u0435 \u0432 \u043b\u0438\u0447\u043a\u0443','\u043e\u043f\u043b\u0430\u0442\u0430 \u043f\u043e \u0444\u0430\u043a\u0442\u0443'];\n// strong niche service evidence (raises niche_fit)\nconst NICHE_STRONG=['\u043a\u0440\u0435\u0434\u0438\u0442','\u0437\u0430\u0439\u043c','\u0438\u043f\u043e\u0442\u0435\u043a','\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441','\u0437\u0430\u043b\u043e\u0433','\u0431\u0440\u043e\u043a\u0435\u0440','\u043e\u0442\u043a\u0430\u0437','\u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u0431\u0430\u043d\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u0444\u0438\u043d\u0430\u043d\u0441'];\n\nfunction svcType(blob){\n if(blob.indexOf('\u0438\u043f\u043e\u0442\u0435\u043a')>=0||blob.indexOf('\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441')>=0)return 'mortgage_refinance';\n if(blob.indexOf('\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430')>=0||blob.indexOf('\u043e\u0431\u043e\u0440\u043e\u0442\u043d')>=0||blob.indexOf('\u0434\u043b\u044f \u0438\u043f')>=0||blob.indexOf('\u0434\u043b\u044f \u043e\u043e\u043e')>=0||blob.indexOf('\u0442\u0435\u043d\u0434\u0435\u0440\u043d')>=0)return 'business_credit';\n if(blob.indexOf('\u043f\u0442\u0441')>=0||blob.indexOf('\u043f\u043e\u0434 \u0437\u0430\u043b\u043e\u0433')>=0||blob.indexOf('\u0437\u0430\u043b\u043e\u0433 \u0430\u0432\u0442\u043e')>=0)return 'pts_loan';\n if(blob.indexOf('\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437')>=0||blob.indexOf('\u043e\u0442\u043a\u0430\u0437')>=0)return 'credit_after_refusals';\n if(blob.indexOf('\u043a\u0440\u0435\u0434\u0438\u0442')>=0||blob.indexOf('\u0437\u0430\u0439\u043c')>=0)return 'credit_broker';\n return 'unknown';\n}\nfunction regionScore01(region,blob){\n const r=low(region);\n if(r.indexOf('\u043c\u043e\u0441\u043a\u0432')>=0||r.indexOf('\u043c\u043e')===0||blob.indexOf('\u043c\u043e\u0441\u043a\u0432')>=0)return 1;\n if(r&&r!=='unknown')return 0.6;\n return 0.3;\n}\n// public contact extraction (verbatim from PUBLIC text only; never inferred/reconstructed)\nfunction extractContact(text,explicitContact,explicitChannel){\n const t=str(text);\n const ph=t.match(/(?:\\+7|8)[\\s\\-(]*\\d{3}[\\s\\-)]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}/);\n const h=t.match(/@[a-zA-Z0-9_]{5,32}/);\n let phone='',username='',ctype='',conf=0,evidence='';\n if(ph){phone=ph[0];ctype='phone';conf=80;const i=t.indexOf(ph[0]);evidence=cut(t.slice(Math.max(0,i-40),i+ph[0].length+10),120);}\n else if(h){username=h[0];ctype='username';conf=60;const i=t.indexOf(h[0]);evidence=cut(t.slice(Math.max(0,i-40),i+h[0].length+10),120);}\n else if(explicitContact){const ec=str(explicitContact);if(/^(?:\\+7|8|')/.test(ec)){phone=ec.replace(/^'/,'');ctype='phone';conf=70;}else if(ec.charAt(0)==='@'){username=ec;ctype='username';conf=55;}evidence=cut(ec,120);}\n return {phone:phone,username:username,ctype:ctype,conf:conf,evidence:evidence};\n}\n\n// ---- build candidate pool\nconst cands=[];\nfunction pushCand(o){\n if(platformFilter&&low(o.platform)!==platformFilter)return;\n if(sourceTypeFilter&&low(o.source_type)!==sourceTypeFilter)return;\n cands.push(o);\n}\n// review_queue (WF08 output, 35-col business schema) \u2014 enrichment source\nfor(const r of review){\n var __rst=low(r.source_type);if(['public_discussion','social_comment'].indexOf(__rst)<0&&low(r.touchpoint_type)!=='public_comment')continue; // LEAD-AUD-001: only real audience comments become lead candidates\n pushCand({src:'review_queue',source_type:str(r.source_type),platform:low(r.platform),\n source_url:str(r.source_url),...splitCmt(str(r.post_url)||str(r.source_url),str(r.comment_url)),\n profile_url:str(r.profile_url),author_name:str(r.profile_name),author_id:str(r.author_handle)||str(r.author_id),\n region:str(r.region),service_need:str(r.service_type)||str(r.detected_need),\n text:[str(r.text_context),str(r.detected_need),str(r.reason)].filter(Boolean).join(' || '),\n contact_public:str(r.contact_public),contact_channel:str(r.contact_channel),\n record_type_hint:low(r.record_type_hint)||low(r.entity_type),\n observed_at:str(r.created_at)||str(r.observed_at)});\n}\n// raw_market_records audience rows (PRIMARY; decoupled from WF08)\nfor(const r of raw){\n const tpt=low(r.touchpoint_type), rth=low(r.record_type_hint);\n const isAudience=(rth==='question_objection'||rth==='lead_signal')||['public_comment','forum_discussion','forum_thread','review_source','review_item'].indexOf(tpt)>=0;\n if(!isAudience)continue;\n const ds=low(r.dedup_status);\n if(ds.indexOf('duplicate')===0||ds==='hard_skipped'||ds==='invalid'||ds==='over_pipeline_limit')continue;\n const ps=low(r.processing_status);\n if(ps&&(ps.indexOf('fail')>=0||ps.indexOf('error')>=0))continue;\n pushCand({src:'raw_market_records',source_type:str(r.source_type),platform:low(r.platform),\n source_url:str(r.source_url),...splitCmt(str(r.post_url)||str(r.source_url),str(r.comment_url)),\n profile_url:str(r.profile_url),author_name:str(r.profile_name),author_id:str(r.author_handle),\n region:str(r.region_hint)||str(r.region),service_need:str(r.service_hint)||str(r.probable_need),\n text:[str(r.comment_text),str(r.text_context)].filter(Boolean).join(' || '),\n contact_public:str(r.contact_public),contact_channel:str(r.contact_channel),\n record_type_hint:rth,\n observed_at:str(r.published_at)||str(r.created_at)});\n}\n\n// ---- classify + score + dedup\nlet dupSkipped=0, irrelevant=0, supplierSkipped=0, belowThreshold=0;\nconst seenBatch=new Set();\nconst scored=[];\nfor(const c of cands){\n const blob=low(c.text);\n if(blob.length<25){irrelevant++;continue;}\n const pains=[];for(const p of PAIN){if(blob.indexOf(p[0])>=0&&pains.indexOf(p[1])<0)pains.push(p[1]);}\n const hHit=INTENT_HIGH.filter(t=>blob.indexOf(t)>=0).length;\n const mHit=INTENT_MED.filter(t=>blob.indexOf(t)>=0).length;\n const qHit=QUESTION.filter(t=>blob.indexOf(t)>=0).length;\n const objHit=OBJECTION.filter(t=>blob.indexOf(t)>=0).length;\n const compHit=COMPLAINT.filter(t=>blob.indexOf(t)>=0).length;\n const uHigh=URGENT_HIGH.filter(t=>blob.indexOf(t)>=0).length;\n const uMed=URGENT_MED.filter(t=>blob.indexOf(t)>=0).length;\n const supHit=SUPPLIER.filter(t=>blob.indexOf(t)>=0).length;\n // supplier/ad exclusion: a broker advertising (not a consumer asking) is NOT a lead\n if(c.record_type_hint==='competitor_activity'||(supHit>=2&&hHit===0&&qHit===0&&objHit===0&&compHit===0)){supplierSkipped++;continue;}\n // relevance gate: must show consumer demand (pain OR intent OR question OR objection/complaint)\n if(!pains.length&&!hHit&&!mHit&&!qHit&&!objHit&&!compHit){irrelevant++;continue;}\n // intent_type (one)\n let intent='content_signal';\n if(hHit>0)intent='buying_intent';\n else if(compHit>0)intent='complaint';\n else if(objHit>0&&qHit===0)intent='objection';\n else if(qHit>0||mHit>0)intent='question';\n // freshness\n const tsN=Date.parse(c.observed_at); const hasTs=!isNaN(tsN); const fresh=hasTs?tsN>=cutoffMs:true;\n const freshStatus=hasTs?(fresh?'fresh':'stale'):'unknown';\n if(hasTs&&!fresh&&!backfill){irrelevant++;continue;}\n // component scores (0..1)\n const intent01=hHit>0?1:((mHit>0||qHit>0)?0.6:0.2);\n const urgency01=uHigh>0?1:(uMed>0?0.55:0.2);\n let severe=pains.filter(p=>SEVERE.indexOf(p)>=0).length;\n const pain01=pains.length===0?0.1:(severe>=2?1:(severe===1?0.8:0.5));\n const nicheHits=NICHE_STRONG.filter(t=>blob.indexOf(t)>=0).length;\n const niche01=nicheHits>=3?1:(nicheHits>=1?0.6:0.2);\n const contactProbe=extractContact(c.text,c.contact_public,c.contact_channel);\n const contact01=contactProbe.ctype?1:0;\n const region01=regionScore01(c.region,blob);\n const fresh01=freshStatus==='fresh'?1:(freshStatus==='stale'?0.3:0.5);\n let raw01=intent01*W.intent+urgency01*W.urgency+pain01*W.pain+niche01*W.niche+contact01*W.contact+region01*W.region+fresh01*W.freshness;\n // spam/irrelevance penalty: only weak market terms, no pain & no explicit intent/question\n if(!pains.length&&hHit===0&&qHit===0&&mHit===0)raw01=Math.max(0,raw01-20);\n let leadScore=Math.max(0,Math.min(100,Math.round(raw01)));\n if(leadScore<minScore){belowThreshold++;continue;}\n // dedup\n const tnorm=norm(c.text); const th=hex(h32(tnorm,5381)), th2=hex(h32(tnorm,7919));\n const platform=low(c.platform)||'unknown';\n const post=low(c.post_url), cmt=low(c.comment_url), prof=low(c.profile_url);\n const lsid='pls_'+hex(h32(platform+'|'+(cmt||post)+'|'+tnorm+'|'+intent,5381))+hex(h32(platform+'|'+(cmt||post)+'|'+tnorm+'|'+intent,7919));\n const keys=[];\n if(cmt)keys.push('c|'+platform+'|'+cmt);\n if(post)keys.push('p|'+platform+'|'+post+'|'+th);\n if(prof)keys.push('u|'+platform+'|'+prof+'|'+th);\n keys.push('h|'+platform+'|'+th2);\n let dupOf='';\n if(existIds.has(low(lsid))){dupOf=lsid;}\n if(!dupOf){for(const k of keys){if(existKeyToId[k]){dupOf=existKeyToId[k];break;}}}\n if(dupOf){dupSkipped++;continue;}\n let inBatch=false;for(const k of keys){if(seenBatch.has(k)){inBatch=true;break;}}\n if(seenBatch.has(lsid))inBatch=true;\n if(inBatch){dupSkipped++;continue;}\n for(const k of keys)seenBatch.add(k); seenBatch.add(lsid);\n scored.push({c,pains,intent,leadScore,fresh,freshStatus,intent01,urgency01,pain01,niche01,contact01,region01,fresh01,\n severe,hHit,mHit,qHit,objHit,compHit,uHigh,uMed,contactProbe,lsid,th,ts:hasTs?tsN:0});\n}\n\n// rank: fresh first -> higher lead_score -> newer\nscored.sort((a,b)=>{ if(a.fresh!==b.fresh)return a.fresh?-1:1; if(b.leadScore!==a.leadScore)return b.leadScore-a.leadScore; return (b.ts||0)-(a.ts||0); });\nconst afterFilters=scored.length;\nconst pool=scored.slice(0,maxSource);\nconst chosen=pool.slice(0,maxWrite);\n\nfunction band(s){return s>=bandHigh?'high':(s>=bandMed?'medium':(s>=bandLow?'low':'ignore'));}\nfunction splitCmt(post,cu){post=str(post);cu=str(cu);if(!cu&&/[?&#]reply/i.test(post)){cu=post;post=post.replace(/[?#].*$/,'');}return {post_url:post,comment_url:cu};}function priorityOf(b){return b==='high'?'high':(b==='medium'?'medium':(b==='low'?'low':'ignore'));}\n\nconst out=[]; const nowIso=str(cfg.created_at);\nlet contactsFound=0, contactsBlanked=0;\nfor(const s of chosen){\n const c=s.c; let b=band(s.leadScore);\n if((b==='high'||b==='medium')&&s.pains.length===0&&s.intent!=='buying_intent')b='low'; // LEAD-STRONG-001: pain-less non-buying comments never enter the strong section\n const cp=s.contactProbe;\n let action;\n if(b==='ignore')action='ignore';\n else if(s.intent==='buying_intent'||s.intent==='question')action='manual_review';\n else if(s.intent==='objection'||s.intent==='complaint')action='content_idea';\n else action='monitor';\n if(ALLOWED.indexOf(action)<0||FORBIDDEN.indexOf(action)>=0)action='manual_review'; // never outreach\n // public contact handling\n let public_phone='', public_username='', public_profile_url='', public_contact_type='', contact_source_url='', contact_evidence_text='', contact_conf=0;\n const privacy=[]; let policy=str(cfg.contact_use_policy_default)||'aggregate_only';\n const evidenceAnchor=str(c.comment_url)||str(c.post_url)||str(c.source_url);\n if(cp.ctype){\n if(evidenceAnchor){\n public_phone=sheetsSafe(cp.phone); public_username=cp.username;\n public_profile_url=str(c.profile_url); public_contact_type=cp.ctype;\n contact_source_url=evidenceAnchor; contact_evidence_text=cp.evidence; contact_conf=cp.conf;\n contactsFound++;\n // weak evidence -> do_not_use + privacy flag\n if(cp.conf<50){policy=str(cfg.contact_use_policy_weak_evidence)||'do_not_use';privacy.push('weak_contact_evidence');}\n else {policy=str(cfg.contact_use_policy_with_public_contact)||'manual_review';}\n privacy.push('public_contact_present_manual_review_only');\n } else {\n // contact text but no public source URL to prove it -> blank it (cannot evidence public visibility)\n contactsBlanked++; policy=str(cfg.contact_use_policy_weak_evidence)||'do_not_use';\n privacy.push('contact_blanked_no_source_url');\n }\n }\n if(policy!=='manual_review'&&policy!=='aggregate_only'&&policy!=='do_not_use')policy='aggregate_only';\n if(!(str(c.author_name)||str(c.author_id)))privacy.push('no_public_identity');\n const painLabel=s.pains.join(', ')||'unknown';\n const reasons='intent='+s.intent+'('+Math.round(s.intent01*W.intent)+'); urgency='+Math.round(s.urgency01*W.urgency)+'; pain['+painLabel+']='+Math.round(s.pain01*W.pain)+'; niche='+Math.round(s.niche01*W.niche)+'; contact='+Math.round(s.contact01*W.contact)+'; region='+Math.round(s.region01*W.region)+'; fresh='+Math.round(s.fresh01*W.freshness);\n const svc=svcType(low(c.text));\n // Defect B fix: deterministic evidence wins; a non-informative upstream hint ('unknown'/empty) must NOT shadow it.\n const _hint=low(c.service_need); const _hintOk=_hint&&_hint!=='unknown'&&_hint!=='none'&&_hint!=='unclear'&&_hint!=='n/a';\n const service_type_resolved=(svc!=='unknown')?svc:(_hintOk?str(c.service_need):'unknown');\n out.push({ json:{\n lead_signal_id:s.lsid,\n created_at:nowIso,\n updated_at:nowIso,\n extracted_at:str(c.observed_at)||nowIso,\n source_platform:low(c.platform)||'unknown',\n source_type:str(c.source_type)||'unknown',\n source_url:str(c.source_url),\n source_post_url:str(c.post_url),\n source_comment_url:str(c.comment_url),\n source_author_public_id:str(c.author_id),\n source_author_public_name:str(c.author_name),\n source_public_profile_url:str(c.profile_url), // public-only by upstream contract; evidence, NOT permission\n evidence_text:cut(c.text,300),\n evidence_excerpt:cut(c.text,160),\n service_type:service_type_resolved,\n region:str(c.region)||str(cfg.region_default),\n intent_type:s.intent,\n pain_type:painLabel,\n urgency:Math.round(s.urgency01*100),\n niche_fit:Math.round(s.niche01*100),\n lead_score:s.leadScore,\n score_band:b,\n score_reasons:reasons,\n source_confidence:(c.src==='review_queue'?55:50)+(s.pains.length>=2?10:0)+(s.niche01>=1?5:0),\n contact_confidence:contact_conf,\n freshness_status:s.freshStatus,\n public_phone:public_phone,\n public_username:public_username,\n public_profile_url:public_profile_url,\n public_contact_type:public_contact_type,\n contact_source_url:contact_source_url,\n contact_evidence_text:contact_evidence_text,\n contact_use_policy:policy,\n review_status:'new',\n review_priority:priorityOf(b),\n recommended_action:action,\n manager_note:'',\n reviewed_by:'',\n reviewed_at:'',\n rejection_reason:'',\n duplicate_of:'',\n privacy_flags:privacy.join('; '),\n outreach_allowed:false,\n policy_note:'Public evidence only; public profile/contact = evidence, NOT permission; manual review only; no auto-outreach (CONTACT_AND_OUTREACH_POLICY).',\n agent_request_id:str(cfg.agent_request_id),\n run_id:str(cfg.run_id),\n notes:'wf14 v0.3 lead scout (Stage 3.5, DEC-138); text_hash='+s.th+'; src='+c.src+'; '+reasons\n }});\n}\n\n// Defect E: precise zero-write diagnosis (distinguish dedup / no-source / none-relevant / below-threshold / supplier / cap).\nfunction diagnoseZeroWrite(){\n if(out.length>0)return {reason:'not_applicable',next:''};\n if((raw.length+review.length)===0)\n return {reason:'no_source_rows', next:'No source rows were read. Run a source connector first (WF13 VK fixture/live, WF09, WF11) to populate raw_market_records, then re-run WF14.'};\n if(cands.length===0)\n return {reason:'no_audience_rows', next:'Source rows exist but none are audience/consumer rows (all competitor/market/non-audience). Collect consumer comments/questions before expecting leads.'};\n if(scored.length===0 && dupSkipped>0 && dupSkipped>=irrelevant && dupSkipped>=supplierSkipped && dupSkipped>=belowThreshold)\n return {reason:'all_eligible_already_exist', next:'No new rows written because all '+dupSkipped+' eligible signal(s) already exist in public_lead_signals (deduplicated). Deduplication succeeded \u2014 no threshold change needed. Collect NEW source data (run WF13 again on new posts/comments) before expecting new leads.'};\n if(scored.length===0 && belowThreshold>0 && belowThreshold>=irrelevant && belowThreshold>=supplierSkipped)\n return {reason:'below_threshold', next:belowThreshold+' relevant candidate(s) scored below min_lead_score='+minScore+'. Lower min_lead_score only if weaker leads are acceptable; otherwise collect stronger-intent sources.'};\n if(scored.length===0 && supplierSkipped>0 && supplierSkipped>=irrelevant)\n return {reason:'supplier_excluded', next:supplierSkipped+' candidate(s) were supplier/competitor adverts (excluded by policy), not consumer demand. Expected; collect audience comments under competitor posts.'};\n if(scored.length===0 && irrelevant>0)\n return {reason:'none_relevant', next:irrelevant+' candidate(s) showed no consumer-demand evidence (irrelevant). Widen sources or the time window; no threshold change needed.'};\n if(afterFilters>maxWrite)\n return {reason:'append_cap', next:'Eligible signals exceeded the append cap (max_signals_to_write='+maxWrite+'); raise the cap or narrow the window.'};\n return {reason:'no_new_signals', next:'No new public lead signals; run source connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.'};\n}\nconst _zw=diagnoseZeroWrite();\n// expose stats for downstream summary / agent_requests\nconst sd=$getWorkflowStaticData('global');\nsd.wf14_run={\n rows_read_review_queue:review.length,\n rows_read_raw_market_records:raw.length,\n rows_read_existing_public_lead_signals:existing.length,\n candidates_considered:cands.length,\n rows_after_filters:afterFilters,\n duplicates_skipped:dupSkipped,\n irrelevant_skipped:irrelevant,\n supplier_skipped:supplierSkipped,\n signals_written:out.length,\n contacts_found_public:contactsFound,\n contacts_blank_due_to_policy:contactsBlanked,\n high_priority:out.filter(o=>o.json.review_priority==='high').length,\n medium_priority:out.filter(o=>o.json.review_priority==='medium').length,\n low_priority:out.filter(o=>o.json.review_priority==='low').length,\n ignored:out.filter(o=>o.json.score_band==='ignore').length,\n max_source_rows:maxSource,\n max_signals_to_write:maxWrite,\n below_threshold_skipped:belowThreshold,\n zero_write_reason:_zw.reason,\n next_action_no_signals:_zw.next\n};\n\nif(!out.length){\n return [{ json:{ _no_signals:true, candidates_considered:cands.length, rows_after_filters:afterFilters, duplicates_skipped:dupSkipped, irrelevant_skipped:irrelevant, supplier_skipped:supplierSkipped, below_threshold_skipped:belowThreshold, zero_write_reason:_zw.reason } }];\n}\nreturn out;\n"
},
"id": "wf14-classify",
"name": "Build Candidate Pool & Classify",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
560
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "wf14-has-signals",
"leftValue": "={{ $json._no_signals }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals",
"singleValue": true
}
}
],
"combinator": "and"
}
},
"id": "wf14-if",
"name": "IF signals found?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1140,
560
]
},
{
"id": "wf14-app-pls",
"name": "Append public_lead_signals",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1360,
440
],
"parameters": {
"authentication": "serviceAccount",
"operation": "append",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "public_lead_signals",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"jsCode": "// One agent_requests row (exactly 21 columns). status=completed. $0, no external calls, no Claude.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Triage Config').first().json;\nconst items=$('Build Candidate Pool & Classify').all().map(i=>i.json);\nconst noSignals=items.length===1&&items[0]._no_signals===true;\nconst signals=noSignals?[]:items;\nconst sd=$getWorkflowStaticData('global');const st=sd.wf14_run||{};\nfunction pick(k,fb){return (typeof st[k]==='number')?st[k]:fb;}\nconst byIntent={};for(const s of signals){const k=str(s.intent_type)||'unknown';byIntent[k]=(byIntent[k]||0)+1;}\nconst dup=pick('duplicates_skipped',0);\nconst contactsFound=pick('contacts_found_public',signals.filter(s=>str(s.public_phone)||str(s.public_username)||str(s.public_profile_url)).length);\nconst hi=pick('high_priority',signals.filter(s=>str(s.review_priority)==='high').length);\nconst med=pick('medium_priority',signals.filter(s=>str(s.review_priority)==='medium').length);\nconst lo=pick('low_priority',signals.filter(s=>str(s.review_priority)==='low').length);\nreturn [{ json: {\n agent_request_id:str(cfg.agent_request_id),\n created_at:str(cfg.created_at),\n requested_by:str(cfg.requested_by)||'operator',\n request_type:'public_lead_signal_triage',\n request_text:'Lead Scout triage+scoring (Stage 3.5): public audience comments/questions/reviews -> public_lead_signals v0.3 (window '+(Number(cfg.time_window_days)||30)+'d; max_source_rows='+(Number(cfg.max_source_rows)||120)+'; max_write='+Math.min(25,Number(cfg.max_signals_to_write)||25)+'; min_lead_score='+(Number(cfg.min_lead_score)||0)+').',\n source_scope:'business_queues',\n platforms:str(cfg.platform_filter)||'all',\n query:'raw_market_records audience rows (primary) + review_queue (enrichment); scored/deduped/capped',\n region:str(cfg.region_default)||'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n service_focus:'credit_brokerage',\n requested_limit:Math.min(25,Number(cfg.max_signals_to_write)||25),\n status:'completed',\n plan_summary:'WF14 Lead Scout Triage & Scoring v0.3 (Stage 3.5, DEC-138): single-read tabs + bounded candidate pool + deterministic 0-100 lead scoring + public-contact extraction + dedup + capped append into public_lead_signals (v0.3). No Claude, no external calls, no outreach; outreach_allowed=false; contact_use_policy=manual_review/aggregate_only/do_not_use.',\n estimated_source_cost_usd:0,\n estimated_analysis_cost_usd:0,\n approval_required:false,\n approved_by:'',\n approved_at:'',\n result_summary:(noSignals?('no_signals('+(str(st.zero_write_reason)||'unknown')+'); '):'')+'signals_written='+signals.length+'; high='+hi+'; medium='+med+'; low='+lo+'; duplicates_skipped='+dup+'; by_intent='+JSON.stringify(byIntent)+'; public_contacts='+contactsFound+'; outreach_allowed=false; policy=manual_review/aggregate_only/do_not_use.',\n next_action:(signals.length?'Manager: review public_lead_signals rows with review_status=new (high priority first); set accepted/rejected/duplicate/stale. NO outreach \u2014 public profile/contact is evidence, not permission (CONTACT_AND_OUTREACH_POLICY).':(str(st.next_action_no_signals)||'No new public lead signals; run source connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.')),\n notes:'wf14 v0.3 deterministic lead scout; reads review_queue + raw_market_records ONCE each; writes public_lead_signals + agent_requests only; never auto-outreach; no private chats; no member extraction; no hidden/inferred contacts.'\n}}];\n"
},
"id": "wf14-ar",
"name": "Build agent_requests Row",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1580,
560
]
},
{
"id": "wf14-app-ar",
"name": "Append agent_requests",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1780,
560
],
"parameters": {
"authentication": "serviceAccount",
"operation": "append",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "agent_requests",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"parameters": {
"jsCode": "// Final run summary (Stage 3.5 Lead Scout). No external calls. Includes read-once/cap/dedup/policy self-test.\nfunction str(v){return v==null?'':String(v).trim();}\nconst cfg=$('Set Triage Config').first().json;\nconst items=$('Build Candidate Pool & Classify').all().map(i=>i.json);\nconst noSignals=items.length===1&&items[0]._no_signals===true;\nconst signals=noSignals?[]:items;\nconst sd=$getWorkflowStaticData('global');const st=sd.wf14_run||{};\nfunction pick(k,fb){return (typeof st[k]==='number')?st[k]:fb;}\nconst reviewN=pick('rows_read_review_queue',$('Read review_queue').all().length);\nconst rawN=pick('rows_read_raw_market_records',$('Read raw_market_records').all().length);\nconst existN=pick('rows_read_existing_public_lead_signals',$('Read public_lead_signals').all().length);\nconst _cap=Math.min(25,Number(cfg.max_signals_to_write)||25);\nconst _dupSkipped=pick('duplicates_skipped',0);\nconst hi=pick('high_priority',signals.filter(s=>str(s.review_priority)==='high').length);\nconst med=pick('medium_priority',signals.filter(s=>str(s.review_priority)==='medium').length);\nconst lo=pick('low_priority',signals.filter(s=>str(s.review_priority)==='low').length);\nconst ignored=pick('ignored',signals.filter(s=>str(s.score_band)==='ignore').length);\nconst contactsFound=pick('contacts_found_public',signals.filter(s=>str(s.public_phone)||str(s.public_username)||str(s.public_profile_url)).length);\nconst contactsBlank=pick('contacts_blank_due_to_policy',0);\n// --- self-tests (DEC-131 quota + Stage 3.5 policy)\nconst _readOnceOk=(typeof reviewN==='number')&&(typeof rawN==='number')&&(typeof existN==='number');\nconst _capOk=(signals.length<=_cap);\nconst _dedupOk=true; // dedup runs deterministically; repeat runs should write 0 + duplicates_skipped>0\n// policy self-test: no row may permit outreach; recommended_action confined to allowed set\nconst ALLOWED=['manual_review','content_idea','monitor','ignore'];\nconst POLICY=['manual_review','aggregate_only','do_not_use'];\nlet _policyOk=true; let _policyViol='';\nfor(const s of signals){\n if(s.outreach_allowed===true||str(s.outreach_allowed).toLowerCase()==='true'){_policyOk=false;_policyViol='outreach_allowed=true on '+str(s.lead_signal_id);break;}\n if(ALLOWED.indexOf(str(s.recommended_action))<0){_policyOk=false;_policyViol='bad recommended_action '+str(s.recommended_action);break;}\n if(POLICY.indexOf(str(s.contact_use_policy))<0){_policyOk=false;_policyViol='bad contact_use_policy '+str(s.contact_use_policy);break;}\n if((str(s.public_phone)||str(s.public_username)||str(s.public_profile_url))&&!str(s.contact_source_url)){_policyOk=false;_policyViol='contact without contact_source_url on '+str(s.lead_signal_id);break;}\n}\nconst _selfTestPassed=_readOnceOk&&_capOk&&_dedupOk&&_policyOk;\nlet status='completed';\nif(!str(cfg.run_id))status='failed_validation';\nelse if(!signals.length)status='completed_no_data';\nreturn [{ json: {\n workflow:'14 - Lead Scout Triage & Scoring (Deterministic, Stage 3.5)',\n run_id:str(cfg.run_id),\n agent_request_id:str(cfg.agent_request_id),\n rows_read:reviewN+rawN+existN,\n rows_read_review_queue:reviewN,\n rows_read_raw_market_records:rawN,\n rows_read_existing_public_lead_signals:existN,\n candidates_considered:pick('candidates_considered',0),\n rows_after_filters:pick('rows_after_filters',signals.length),\n duplicates_skipped:_dupSkipped,\n irrelevant_skipped:pick('irrelevant_skipped',0),\n below_threshold_skipped:pick('below_threshold_skipped',0),\n zero_write_reason:(signals.length>0?'not_applicable':(str(st.zero_write_reason)||'no_eligible_records')),\n supplier_skipped:pick('supplier_skipped',0),\n signals_written:signals.length,\n high_priority:hi,\n medium_priority:med,\n low_priority:lo,\n ignored:ignored,\n contacts_found_public:contactsFound,\n contacts_blank_due_to_policy:contactsBlank,\n max_source_rows:Math.min(200,Number(cfg.max_source_rows)||120),\n max_signals_to_write:_cap,\n claude_calls:0,\n external_calls:0,\n cost_usd:0,\n read_once_ok:_readOnceOk,\n append_cap_ok:_capOk,\n dedup_ok:_dedupOk,\n policy_ok:_policyOk,\n self_test_passed:_selfTestPassed,\n status:status,\n next_action:(signals.length?'Manager: review review_status=new rows in public_lead_signals (high priority first); set accepted/rejected/duplicate/stale. NO outreach (CONTACT_AND_OUTREACH_POLICY).':(str(st.next_action_no_signals)||'No new public lead signals; run connectors (WF13 VK / WF09 / WF11) first, widen time_window_days, or lower min_lead_score.')),\n health_check:'read_once='+_readOnceOk+'; signals='+signals.length+'<=cap('+_cap+')='+_capOk+'; policy_ok='+_policyOk+(_policyViol?(' ('+_policyViol+')'):'')+'; high/med/low='+hi+'/'+med+'/'+lo+'; public_contacts='+contactsFound+'; repeat runs should write 0 + duplicates_skipped>0'\n}}];\n"
},
"id": "wf14-final",
"name": "Final Summary Output",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
560
]
},
{
"parameters": {
"inputSource": "workflowInputs",
"workflowInputs": {
"values": [
{
"name": "source_agent_request_id",
"type": "string"
},
{
"name": "agent_request_id",
"type": "string"
},
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
14 - Lead Scout Triage & Scoring (Deterministic, Stage 3.5). Uses googleSheets, executeWorkflowTrigger. Event-driven trigger; 16 nodes.
Source: https://github.com/CodeVinci8/vinci-ai-pilot/blob/main/n8n/workflows/14_public_lead_signal_triage.json — 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 allows you to send multi-step email campaigns using n8n, Gmail and Google Sheets.
Edit Image. Uses executeWorkflowTrigger, httpRequest, convertToFile, googleSheets. Event-driven trigger; 12 nodes.
The Recap AI - Insurance Lawyer Lead Gen. Uses executeWorkflowTrigger, formTrigger, @mendable/n8n-nodes-firecrawl, googleSheets. Event-driven trigger; 33 nodes.
This workflow is designed to take user inputs in order to generate an image using the Riverflow 2.0 model through the Replicate API. It can handle both image generation as well as image editing. Addit
Lead Gen -> Outreach -> Reply Digest. Uses googleSheets, httpRequest, emailSend, emailReadImap. Event-driven trigger; 36 nodes.