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": "10 - Competitor Audience Intelligence Aggregator",
"nodes": [
{
"id": "aa100000-0000-0000-0000-0000000000n1",
"name": "Overview Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-460,
-300
],
"parameters": {
"content": "## 10 \u2014 Competitor/Audience Intelligence Aggregator v0.3 \u2014 DEC-104/106/107/108/127\n\nWF08 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u00ab\u0447\u0442\u043e \u044d\u0442\u043e \u0437\u0430 \u043e\u0434\u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c?\u00bb. WF10 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u00ab\u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u0430 \u0440\u044b\u043d\u043a\u0435 \u0432 \u0446\u0435\u043b\u043e\u043c?\u00bb.\n\n\u0427\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442 (\u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u041e, $0, \u0431\u0435\u0437 LLM/Apify/Firecrawl):\n1. \u0420\u0443\u0447\u043d\u043e\u0439 \u0441\u0442\u0430\u0440\u0442 \u2192 Set Aggregator Config (\u043e\u043a\u043d\u043e 30 \u0434\u043d\u0435\u0439; \u0444\u0438\u043b\u044c\u0442\u0440\u044b niche/platform/region/service_type).\n2. \u0427\u0438\u0442\u0430\u0435\u0442 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043a\u0438: monitor_queue, content_queue, review_queue (+ source_confidence_rules \u0434\u043b\u044f seed-\u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438).\n3. Aggregate Market Intelligence: \u0444\u0438\u043b\u044c\u0442\u0440 \u043f\u043e \u043e\u043a\u043d\u0443/\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u043c \u2192 \u0433\u0440\u0443\u043f\u043f\u0438\u0440\u043e\u0432\u043a\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (company_name \u2192 profile_name \u2192 offer_text+platform \u2192 listing id \u0438\u0437 source_url) \u2192 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0435 \u043e\u0444\u0444\u0435\u0440\u043e\u0432/\u0446\u0435\u043d/\u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0438/\u0431\u043e\u043b\u0435\u0439 \u2192 \u043f\u043e\u0434\u0441\u0447\u0451\u0442 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449\u0438\u0445\u0441\u044f \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u0443\u0433\u043b\u043e\u0432 (\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c, \u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c, \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b, \u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442, \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432, \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418, \u0431\u0438\u0437\u043d\u0435\u0441-\u0444\u0438\u043d\u0430\u043d\u0441\u044b, \u0438\u043f\u043e\u0442\u0435\u043a\u0430/\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441, \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438).\n4. \u041f\u0438\u0448\u0435\u0442: competitor_profiles, market_angles, audience_activity_signals (\u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b, \u0431\u0435\u0437 \u0430\u0432\u0442\u043e\u0440\u043e\u0432/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432), content_positioning_plan (1 \u0441\u0442\u0440\u043e\u043a\u0430/\u043f\u0440\u043e\u0433\u043e\u043d), source_confidence_rules (seed \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 \u0432\u043a\u043b\u0430\u0434\u043a\u0430 \u043f\u0443\u0441\u0442\u0430), agent_requests (1 \u0441\u0442\u0440\u043e\u043a\u0430, status=completed).\n\n\u0427\u0415\u0413\u041e \u041d\u0415 \u0414\u0415\u041b\u0410\u0415\u0422: \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 API; \u043d\u0435 \u0441\u043a\u0440\u0435\u0439\u043f\u0438\u0442; \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Claude; \u043d\u0435 \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0435\u0442 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b; \u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442 outreach (DEC-098); \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u0442 raw_market_records/registry/\u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043d\u0430 \u0437\u0430\u043f\u0438\u0441\u044c. active=false; \u0437\u0430\u043f\u0443\u0441\u043a \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0440\u0443\u0447\u043d\u0443\u044e. Niche-\u0441\u043b\u043e\u0432\u0430\u0440\u044c v0.1 \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d (credit_brokerage) \u2014 \u0432\u044b\u043d\u043e\u0441 \u0432 niche packs \u043f\u043e NICHE_PACK_SYSTEM_PLAN.\n\n**v0.2 (2026-06-12):** 1) no-data guard \u2014 \u043f\u0440\u0438 rows_after_filters=0 \u043f\u0438\u0448\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 no_data \u043f\u043b\u0430\u043d (\u043f\u0443\u0441\u0442\u044b\u0435 top_angles/posts/ads/faq/counterarguments/lead_magnets, source_evidence=rows=0, next_action=no_data) + agent_requests result_summary \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 no_data; 2) entity resolution \u2014 company_name \u2192 profile_url \u2192 canonical listing id (source_url) \u2192 profile_name \u2192 offer+platform (fallback) \u2014 \u043c\u0435\u043d\u044c\u0448\u0435 \u0434\u0443\u0431\u043b\u0435\u0439 (unnamed) \u0434\u043b\u044f \u043e\u0434\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u044f; 3) source_mix \u043c\u0435\u0442\u043a\u0430 \u0432 notes/result_summary: mixed: live + historical/manual + web pipeline \u2014 \u043e\u0442\u0447\u0451\u0442\u044b \u043d\u0435 \u0432\u044b\u0434\u0430\u044e\u0442 \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430 \u0441\u0432\u0435\u0436\u0438\u0439 live-\u0441\u0431\u043e\u0440.\n\n**v0.3 (DEC-127):** audience_activity_signals \u0441\u0447\u0438\u0442\u0430\u0435\u0442 objection_count \u043f\u043e \u0441\u043b\u043e\u0432\u0430\u0440\u044e \u043d\u0435\u0434\u043e\u0432\u0435\u0440\u0438\u044f (\u0431\u043e\u044e\u0441\u044c \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432 / \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u0430 / \u0431\u0435\u0440\u0443\u0442 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430\u044e\u0442 / \u043e\u0431\u043c\u0430\u043d / \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u044f / \u0434\u043e\u0433\u043e\u0432\u043e\u0440) \u0422\u041e\u041b\u042c\u041a\u041e \u043f\u043e \u0441\u0442\u0440\u043e\u043a\u0430\u043c review_queue (\u0433\u043e\u043b\u043e\u0441 \u0430\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u0438 \u2014 \u0440\u0435\u043a\u043b\u0430\u043c\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u043d\u0435 \u0438\u043d\u0444\u043b\u0438\u0440\u0443\u0435\u0442 \u0441\u0447\u0451\u0442\u0447\u0438\u043a); top_pains \u0443\u043a\u0440\u0443\u043f\u043d\u0435\u043d\u044b: \u00ab\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418\u00bb, \u00ab\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432\u00bb; blob \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 comment_text. \u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 aggregate-only \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439.",
"height": 940,
"width": 620,
"color": 4
}
},
{
"id": "aa100000-0000-0000-0000-0000000000n2",
"name": "Setup and Test Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
200,
-300
],
"parameters": {
"content": "## \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442\n\n1. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c (active=false).\n2. \u0421\u041e\u0417\u0414\u0410\u0422\u042c 5 \u043d\u043e\u0432\u044b\u0445 \u0432\u043a\u043b\u0430\u0434\u043e\u043a \u0441 \u043a\u043e\u043b\u043e\u043d\u043a\u0430\u043c\u0438 \u0438\u0437 docs/WF10_TABLE_SCHEMAS.md: competitor_profiles (17), market_angles (9), audience_activity_signals (14), content_positioning_plan (12), source_confidence_rules (5).\n3. \u041f\u043e\u0441\u043b\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b Google Sheets \u043d\u0430 \u0412\u0421\u0415\u0425 sheet-\u043d\u043e\u0434\u0430\u0445 (4 read + 5 append + agent_requests) \u0438 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 ID.\n4. \u0422\u0435\u0441\u0442 1 (\u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d): Execute once \u2192 competitor_profiles \u22651 (\u0438\u0437 live-\u0441\u0442\u0440\u043e\u043a WF09\u2192WF08), market_angles \u22651, audience_activity_signals = \u043f\u043e 1 \u043d\u0430 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0443, content_positioning_plan +1, source_confidence_rules +7 (seed), agent_requests +1 (completed). \u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c $0 (0 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0432\u044b\u0437\u043e\u0432\u043e\u0432).\n5. \u0422\u0435\u0441\u0442 2 (\u043f\u043e\u0432\u0442\u043e\u0440): source_confidence_rules +0 (seed \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u043f\u0443\u0441\u0442\u043e\u0439 \u0432\u043a\u043b\u0430\u0434\u043a\u0435); \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u044e\u0442 \u043d\u043e\u0432\u044b\u0439 snapshot (append-only v0.1; upsert \u2014 v0.2).\n6. \u041e\u043a\u043d\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 30 \u0434\u043d\u0435\u0439; \u043f\u0443\u0441\u0442\u043e\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u2192 Final Summary \u043f\u043e\u0434\u0441\u043a\u0430\u0436\u0435\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u043e\u043a\u043d\u043e/\u0444\u0438\u043b\u044c\u0442\u0440\u044b.\n\n\u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c: $0 \u0432\u0441\u0435\u0433\u0434\u0430 (\u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0430\u0433\u0440\u0435\u0433\u0430\u0446\u0438\u044f, \u0431\u0435\u0437 LLM).\n7. \u0422\u0435\u0441\u0442 no-data (v0.2): \u0437\u0430\u0434\u0430\u0442\u044c \u0437\u0430\u0432\u0435\u0434\u043e\u043c\u043e \u043f\u0443\u0441\u0442\u043e\u0439 \u0444\u0438\u043b\u044c\u0442\u0440 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 region_filter='\u041d\u0438\u0433\u0434\u0435\u0433\u0440\u0430\u0434') \u2192 Execute once \u2192 competitor_profiles/market_angles/audience_activity_signals +0; content_positioning_plan +1 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 no_data (\u0432\u0441\u0435 \u0441\u043f\u0438\u0441\u043a\u0438 \u043f\u0443\u0441\u0442\u044b\u0435, source_evidence=rows=0); agent_requests result_summary \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0441 'no_data;'. \u0412\u0435\u0440\u043d\u0443\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440 \u043f\u043e\u0441\u043b\u0435 \u0442\u0435\u0441\u0442\u0430.\n8. \u0422\u0435\u0441\u0442 v0.3 (\u043f\u043e\u0441\u043b\u0435 WF13->WF08): \u0441\u0442\u0440\u043e\u043a\u0430 audience_activity_signals \u0434\u043b\u044f vk: question_count>=2, objection_count>=1, buying_intent_count>=1; top_pains \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u00ab\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432\u00bb, \u00ab\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418\u00bb, \u00ab\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432\u00bb.",
"height": 860,
"width": 580,
"color": 5
}
},
{
"id": "aa100000-0000-0000-0000-000000000001",
"name": "Manual Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-380,
520
],
"parameters": {}
},
{
"id": "aa100000-0000-0000-0000-0000000000cf",
"name": "Set Aggregator Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-160,
520
],
"parameters": {
"jsCode": "// WF10 aggregator config. Deterministic ids; MSK timestamps; no external calls, no LLM (DEC-104).\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 now=moscowIsoNow();\nconst stamp=moscowStamp();\nvar __cfg = {\n time_window_days:30,\n enable_historical_comparison:false, // \u00a72 historical trend baseline is OPT-IN, never automatic contamination\n niche_id:'credit_brokerage',\n platform_filter:'',\n region_filter:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n service_type_filter:'',\n max_examples_per_item:3,\n write_confidence_seed_rules_if_empty:true,\n requested_by:'operator',\n request_type:'market_intelligence_aggregation',\n spreadsheet_id_placeholder:'PASTE_SPREADSHEET_ID',\n // --- Stage C Closure Patch 2: strict run isolation + WF16 source_health enforcement (S3-D5, \u00a710) ---\n source_run_id_filter:'',\n agent_request_id_filter:'',\n data_mode_filter:'', // '', live, fixture, manual_test\n report_eligible_filter:'', // '', 'true', 'false'\n quality_status_filter:'', // '', healthy, degraded, quarantined\n review_status_filter:'', // '', pending, accepted, ...\n enforce_source_health:true, // default ON: drop rows whose source run is not report-eligible\n require_source_health:true, // \u00a72 PRODUCTION fail-closed: a row lacking a source_health match is EXCLUDED (set false only for dev/back-compat)\n allow_degraded_report:false, // explicit operator opt-in to include degraded runs (with quality warning)\n allow_fixture_report:false, // explicit operator opt-in to include fixture/manual_test runs (watermark)\n allow_unverified_source:false, // PRODUCTION fail-closed: records with no source_health match AND no live/healthy self-attestation are excluded; set true ONLY for non-production/dev runs\n agent_request_id:'wf10_req_'+stamp,\n run_id:'wf10_'+stamp,\n created_at:now\n};\n\nvar __agentIn=(typeof $json==='object'&&$json)?$json:{};\nfunction __ov(ck,ik){var k=ik||ck;var v=__agentIn[k];if(v!==undefined&&v!==null&&v!=='')__cfg[ck]=v;}\n__ov('agent_request_id_filter','agent_request_id');\n__ov('source_run_id_filter','source_run_id');\n__ov('data_mode_filter','data_mode');\n__ov('platform_filter','platform_filter');\n__ov('region_filter','region_filter');\n__ov('service_type_filter','service_type_filter');\n__ov('report_eligible_filter','report_eligible_filter');\n__ov('quality_status_filter','quality_status_filter');\n__ov('review_status_filter','review_status_filter');\n__ov('time_window_days','time_window_days');\n__ov('niche_id','niche_id');\nreturn [{ json: __cfg }];"
}
},
{
"id": "aa100000-0000-0000-0000-0000000000r1",
"name": "Read monitor_queue",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
60,
520
],
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"parameters": {
"authentication": "serviceAccount",
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "monitor_queue",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"id": "aa100000-0000-0000-0000-0000000000r2",
"name": "Read content_queue",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
280,
520
],
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"parameters": {
"authentication": "serviceAccount",
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "content_queue",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"id": "aa100000-0000-0000-0000-0000000000r3",
"name": "Read review_queue",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
500,
520
],
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"parameters": {
"authentication": "serviceAccount",
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "review_queue",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"id": "aa100000-0000-0000-0000-0000000000r4",
"name": "Read source_confidence_rules",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
720,
520
],
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"parameters": {
"authentication": "serviceAccount",
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "source_confidence_rules",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"id": "aa100000-0000-0000-0000-0000000000ag",
"name": "Aggregate Market Intelligence",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
940,
520
],
"parameters": {
"jsCode": "function runFamilyMatch(rid,f){rid=String(rid==null?'':rid).trim();f=String(f==null?'':f).trim();if(!f)return true;if(!rid)return false;return rid===f||rid.indexOf(f+'::')===0||f.indexOf(rid+'::')===0;}\n// --- RUN-LINEAGE-001: embedded n8n/lib/run_lineage.js (drift-proof; test asserts equality) ---\n// run_lineage.js \u2014 RUN-LINEAGE-001. ONE resolver for \"which run/request/owner does this row belong to?\".\n//\n// This defect class has now cost five separate live failures, each one a silent zero-row report:\n// ISO-ARID-001 \u2014 WF10 strict-compared an absent agent_request_id (28 -> 0 rows)\n// ISO-RUNID-001 \u2014 WF10 read source_run_id||agent_request_id; WF04 writes run_id (308 -> 0 rows)\n// data_mode \u2014 WF10 strict-compared an empty data_mode (all rows dropped)\n// no_lineage \u2014 report_gate reads ONLY source_run_id; queue rows carry run_id (row excluded)\n// region \u2014 an inferred default overrode an explicit user scope (fixed in scope_policy)\n// Every one was the same shape: a consumer strict-compares a field its producer never populates, and the pipeline\n// silently returns nothing while telling the user there is no data. The cure is not another local fallback in the\n// next workflow \u2014 it is ONE resolver every consumer shares.\n//\n// Rules:\n// * an EMPTY optional field is not a mismatch when another canonical field carries the same identity;\n// * owner isolation is mandatory and is NEVER relaxed by this resolver;\n// * the current run may not be satisfied by an unrelated historical row;\n// * malformed/absent lineage fails safely AND is observable (resolved_from tells you what matched).\n//\n// Embeddable: unique rl*-prefixed names, no cross-lib require.\n\nfunction rlStr(v) { return v == null ? '' : String(v).trim(); }\nfunction rlLow(v) { return rlStr(v).toLowerCase(); }\n\n// A source run id is `<request>::<source>::<slot>`; its family root is the request id.\nfunction rlFamilyRoot(id) {\n var s = rlStr(id);\n var i = s.indexOf('::');\n return i > 0 ? s.slice(0, i) : s;\n}\n\n// resolveRunLineage(row) -> { source_run_id, agent_request_id, run_id, family_root, owner_user_id, data_mode,\n// source_id, has_lineage, resolved_from }\n// ONLY the explicitly supported canonical/legacy fields are consulted \u2014 in one documented order.\nfunction resolveRunLineage(row) {\n row = row || {};\n var srid = rlStr(row.source_run_id);\n var arid = rlStr(row.agent_request_id);\n var rid = rlStr(row.run_id);\n var from = [];\n if (srid) from.push('source_run_id');\n if (arid) from.push('agent_request_id');\n if (rid) from.push('run_id');\n\n // The most specific run identity available. WF04's queue rows carry it ONLY in run_id.\n var sourceRun = srid || rid || '';\n // The request family: an explicit agent_request_id wins; otherwise derive it from whichever run id we have.\n var request = arid || rlFamilyRoot(srid || rid) || '';\n var family = rlFamilyRoot(sourceRun || request);\n\n return {\n source_run_id: sourceRun,\n agent_request_id: request,\n run_id: rid,\n family_root: family,\n owner_user_id: rlStr(row.owner_user_id),\n data_mode: rlLow(row.data_mode),\n source_id: rlStr(row.source_url || row.normalized_source_url || row.source_id || row.source_key),\n has_lineage: !!(sourceRun || request),\n resolved_from: from.join('|') || 'none'\n };\n}\n\n// Does `row` belong to the run identified by `filter`? `filter` may be a request id (req_x) or a source run id\n// (req_x::website::a1); a row matches its own request family either way.\nfunction rlFamilyMatch(rowLineage, filter) {\n var f = rlStr(filter);\n if (!f) return true; // no filter = no constraint\n if (!rowLineage || !rowLineage.has_lineage) return false; // fail safe, but ONLY when genuinely absent\n var ids = [rowLineage.source_run_id, rowLineage.agent_request_id, rowLineage.run_id].filter(Boolean);\n for (var i = 0; i < ids.length; i++) {\n var id = ids[i];\n if (id === f) return true;\n if (id.indexOf(f + '::') === 0) return true; // row is a source run inside the requested family\n if (f.indexOf(id + '::') === 0) return true; // filter is a source run inside the row's family\n }\n return false;\n}\n\n// Owner isolation. An unowned (system) row is visible to its own run; a row owned by SOMEONE ELSE never is.\nfunction rlOwnerMatch(rowLineage, owner) {\n var o = rlStr(owner);\n if (!o) return true;\n var ro = rlStr(rowLineage && rowLineage.owner_user_id);\n if (!ro) return true; // unowned/system row \u2014 isolation is enforced by the run family above\n return ro === o;\n}\n\n// The one predicate a consumer should call: does this row belong to THIS run, for THIS owner?\n// opts: { source_run_id_filter, agent_request_id_filter, owner_user_id, data_mode_filter }\nfunction rlRowInScope(row, opts) {\n opts = opts || {};\n var lin = resolveRunLineage(row);\n if (!rlOwnerMatch(lin, opts.owner_user_id)) return { in_scope: false, reason: 'owner_mismatch', lineage: lin };\n if (rlStr(opts.source_run_id_filter) && !rlFamilyMatch(lin, opts.source_run_id_filter)) return { in_scope: false, reason: 'source_run_mismatch', lineage: lin };\n if (rlStr(opts.agent_request_id_filter) && !rlFamilyMatch(lin, opts.agent_request_id_filter)) return { in_scope: false, reason: 'request_mismatch', lineage: lin };\n // ABSENT is not a MISMATCH: a row that never recorded a data_mode is not evidence of the wrong mode.\n if (rlStr(opts.data_mode_filter) && lin.data_mode && lin.data_mode !== rlLow(opts.data_mode_filter)) {\n return { in_scope: false, reason: 'data_mode_mismatch', lineage: lin };\n }\n return { in_scope: true, reason: '', lineage: lin };\n}\n// --- end embedded run_lineage ---\n// --- Stage C Closure Patch 2: embedded n8n/lib/report_gate.js (drift-proof; test asserts equality) ---\n// report_gate.js \u2014 Stage C Closure Patch 2: the source_health gate consumed by WF10 + WF12.\n//\n// Pure, deterministic, NO network, $0. Turns a set of WF16 `source_health` rows into the run-level\n// eligibility decisions that WF10 (aggregation) and WF12 (stakeholder report) MUST enforce before a run\n// can appear as production market intelligence. WF10/WF12 each embed a mirror of these functions; the\n// offline harness runs the real node code and asserts it equals this library (drift-proof, like WF16).\n//\n// A source run is EXCLUDED by default when any of these hold (section 4 of the closure brief):\n// report_eligible=false, quality_status=quarantined, data_mode=fixture, data_mode=manual_test,\n// review_status=pending, semantic validation failed, stale beyond the freshness window,\n// no compatible baseline (for trend comparison only \u2014 handled by selectBaseline).\n// Degraded data is excluded by default and may be included ONLY through an explicit operator opt-in\n// (allow_degraded_report=true), and then it MUST carry a visible quality warning. Fixture / manual_test\n// data may be included ONLY when the operator explicitly generates a fixture report (allow_fixture_report)\n// and then a TEST/FIXTURE watermark MUST be shown.\n\n// RUN-LINEAGE-001: the ONE resolver every consumer shares (destructured so the embed step inlines it).\n\nfunction str(v) { return v == null ? '' : String(v).trim(); }\nfunction low(v) { return str(v).toLowerCase(); }\nfunction bool(v) {\n if (v === true) return true;\n if (v === false) return false;\n const s = low(v);\n return s === 'true' || s === 'yes' || s === '1';\n}\n\n// Run-level exclusion reasons we surface to the operator (stable enum).\nconst EXCLUDE = {\n FIXTURE: 'data_mode=fixture',\n MANUAL_TEST: 'data_mode=manual_test',\n QUARANTINED: 'quality_status=quarantined',\n PENDING: 'review_status=pending',\n SEMANTIC: 'semantic_validation_failed',\n STALE: 'stale_source',\n NOT_ELIGIBLE: 'report_eligible=false',\n DEGRADED_NO_OPTIN: 'degraded_without_opt_in'\n};\n\n// Decide eligibility for ONE source_health row given the report/aggregation config.\nfunction decideRun(h, cfg) {\n h = h || {};\n cfg = cfg || {};\n const allowDegraded = cfg.allow_degraded_report === true;\n const allowFixture = cfg.allow_fixture_report === true;\n const id = str(h.source_run_id);\n const data_mode = low(h.data_mode) || 'live';\n const quality_status = low(h.quality_status);\n const review_status = low(h.review_status);\n const flags = low(h.quality_flags);\n const reportEligibleFlag = (h.report_eligible === undefined) ? null : bool(h.report_eligible);\n\n const reasons = [];\n let degraded_included = false;\n let warning = '';\n\n if (data_mode === 'fixture') { if (allowFixture) warning = 'fixture'; else reasons.push(EXCLUDE.FIXTURE); }\n if (data_mode === 'manual_test') { if (allowFixture) warning = warning || 'manual_test'; else reasons.push(EXCLUDE.MANUAL_TEST); }\n if (quality_status === 'quarantined') reasons.push(EXCLUDE.QUARANTINED);\n // PENDING-MINORITY-001: a run-level `pending_review` FLAG only means SOME records in the run are pending \u2014\n // WF16 sets report_eligible=false ONLY when the WHOLE run is pending (pending===total), which is enforced\n // below via NOT_ELIGIBLE. So a minority-pending run must stay eligible here and let the per-record gate\n // (rowEligible) drop the individual pending rows; otherwise one pending sibling poisons every clean,\n // report_candidate record from the same source run. Only a run-level review_status=pending excludes the run.\n if (review_status === 'pending') reasons.push(EXCLUDE.PENDING);\n if (/semantic_validation_failed/.test(flags)) reasons.push(EXCLUDE.SEMANTIC);\n if (/(^|; )stale_source/.test(flags)) reasons.push(EXCLUDE.STALE);\n\n if (quality_status === 'degraded') {\n if (allowDegraded) { degraded_included = true; warning = warning || 'degraded'; }\n else reasons.push(EXCLUDE.DEGRADED_NO_OPTIN);\n }\n\n // WF16's own report_eligible=false is authoritative, UNLESS an explicit opt-in path (degraded/fixture)\n // is covering this run. This keeps WF16 the single source of truth while honoring operator overrides.\n const optedIn = degraded_included || (allowFixture && (data_mode === 'fixture' || data_mode === 'manual_test'));\n if (reportEligibleFlag === false && !optedIn && reasons.indexOf(EXCLUDE.NOT_ELIGIBLE) < 0) {\n reasons.push(EXCLUDE.NOT_ELIGIBLE);\n }\n\n const eligible = reasons.length === 0;\n return {\n source_run_id: id,\n eligible: eligible,\n degraded_included: eligible && degraded_included,\n quality_status: quality_status,\n data_mode: data_mode,\n review_status: review_status || (/(^|; )pending_review/.test(flags) ? 'pending' : ''),\n warning: eligible ? warning : '',\n reason: reasons.join('; ')\n };\n}\n\n// Build the eligibility index for a set of source_health rows.\nfunction buildEligibility(healthRows, cfg) {\n healthRows = Array.isArray(healthRows) ? healthRows.filter(Boolean) : [];\n const decisions = healthRows.map(function (h) { return decideRun(h, cfg); });\n const index = {};\n decisions.forEach(function (d) { if (d.source_run_id) index[d.source_run_id] = d; });\n const eligible = decisions.filter(function (d) { return d.eligible; });\n const warnings = eligible.filter(function (d) { return d.warning; })\n .map(function (d) { return { source_run_id: d.source_run_id, warning: d.warning }; });\n return {\n decisions: decisions,\n index: index,\n eligible_run_ids: eligible.map(function (d) { return d.source_run_id; }),\n degraded_run_ids: eligible.filter(function (d) { return d.degraded_included; }).map(function (d) { return d.source_run_id; }),\n excluded_run_ids: decisions.filter(function (d) { return !d.eligible; }).map(function (d) { return d.source_run_id; }),\n warnings: warnings,\n has_warning: warnings.length > 0,\n has_fixture_warning: warnings.some(function (w) { return w.warning === 'fixture' || w.warning === 'manual_test'; }),\n has_degraded_included: eligible.some(function (d) { return d.degraded_included; })\n };\n}\n\n// Per-record gate (Patch 3): merge RECORD-LOCAL lineage with the matching source_health verdict \u2014 the\n// stricter of the two wins \u2014 and FAIL CLOSED in production. A record enters intelligence only when it is\n// affirmatively verified eligible: either source_health matched-and-eligible, or the record self-attests\n// live + healthy + report-eligible. Unverified records (no health match AND no affirmative local lineage,\n// or missing source_run_id) are EXCLUDED by default; including them requires the explicit operator override\n// cfg.allow_unverified_source=true (a non-production setting). This does not rely on a source_health join\n// alone, so it still gates correctly when source_health is empty or the run is unscored.\nfunction rowEligible(row, elig, cfg) {\n row = row || {};\n cfg = cfg || {};\n const allowFixture = cfg.allow_fixture_report === true;\n const allowDegraded = cfg.allow_degraded_report === true;\n const allowUnverified = cfg.allow_unverified_source === true; // explicit non-production/operator fail-open\n // RUN-LINEAGE-001: the row's identity comes from the SHARED resolver, not from one field. WF04's queue rows\n // carry the family ONLY in run_id, so reading source_run_id alone reported `no_lineage` for a row that had\n // perfectly good lineage (live: WF10 exec 932). Same defect class as ISO-ARID-001 / ISO-RUNID-001.\n const __lin = resolveRunLineage(row);\n const id = __lin.source_run_id || __lin.agent_request_id;\n const reasons = [];\n let warning = '';\n\n // --- source_health verdict (authoritative when the run is matched) ---\n let healthMatched = false, healthEligible = false, healthDegradedIncluded = false;\n if (elig && elig.index && id && Object.prototype.hasOwnProperty.call(elig.index, id)) {\n healthMatched = true;\n const d = elig.index[id];\n healthEligible = d.eligible;\n healthDegradedIncluded = d.degraded_included;\n if (!d.eligible) reasons.push('run_excluded:' + d.reason);\n else warning = d.warning || '';\n }\n\n // --- record-local lineage (always evaluated; the stricter of local/health wins) ---\n const dm = low(row.data_mode);\n const qs = low(row.quality_status);\n const review = low(row.review_status);\n const parse = low(row.parse_method);\n const flags = low(row.quality_flags);\n const reportFlag = row.report_eligible;\n const reportFalse = (reportFlag === false || String(reportFlag).toLowerCase() === 'false');\n if (dm === 'fixture' && !allowFixture) reasons.push(EXCLUDE.FIXTURE);\n if (dm === 'manual_test' && !allowFixture) reasons.push(EXCLUDE.MANUAL_TEST);\n if (qs === 'quarantined') reasons.push(EXCLUDE.QUARANTINED);\n if (qs === 'degraded' && !allowDegraded) reasons.push(EXCLUDE.DEGRADED_NO_OPTIN);\n if (review === 'pending' || /(^|; )pending_review/.test(flags)) reasons.push(EXCLUDE.PENDING);\n if (parse === 'deterministic_uncertain_no_llm') reasons.push('parse_method=deterministic_uncertain_no_llm');\n if (reportFalse) reasons.push(EXCLUDE.NOT_ELIGIBLE);\n if (/(^|; )stale_source/.test(flags)) reasons.push(EXCLUDE.STALE);\n\n // --- verification gate: production requires affirmative eligibility evidence (fail closed) ---\n // PARSE-OUTCOME-001 (operator decision): a SINGLE successful bounded repair whose payload passed full local\n // validation is `accepted_with_repair` \u2014 audited and confidence-capped, but reportable exactly like healthy.\n // A failed repair / fallback-only / invalid result still lands in degraded|quarantined and stays fail-closed.\n const selfAttestsLive = (dm === 'live' || dm === 'production') &&\n (qs === '' || qs === 'healthy' || qs === 'accepted_with_repair' || (qs === 'degraded' && allowDegraded)) && !reportFalse;\n // explicit operator opt-in to a fixture/manual report verifies that data too (it is watermarked downstream)\n const fixtureOptedIn = allowFixture && (dm === 'fixture' || dm === 'manual_test');\n const verified = (healthMatched && healthEligible) || (selfAttestsLive && cfg.require_source_health !== true) || fixtureOptedIn;\n if (reasons.length === 0 && !verified && !allowUnverified) {\n reasons.push(healthMatched ? 'unverified' : (id ? 'no_source_health' : 'no_lineage'));\n }\n\n const eligible = reasons.length === 0;\n const degraded_included = eligible && (healthDegradedIncluded || (qs === 'degraded' && allowDegraded));\n return { eligible: eligible, reason: reasons.join('; '), warning: eligible ? warning : '', degraded_included: degraded_included };\n}\n\n// WF12 trend baseline: pick the most recent PRIOR run that is comparable to the current snapshot.\n// Compatible = same niche/region/service filters, live/compatible data_mode, report eligible, non-empty,\n// and within a comparable time window. When none qualifies -> trend_status=no_compatible_baseline and\n// items must NOT all be marked NEW.\nfunction selectBaseline(candidates, current, cfg) {\n candidates = Array.isArray(candidates) ? candidates.filter(Boolean) : [];\n current = current || {};\n cfg = cfg || {};\n const cn = low(current.niche_id), cr = low(current.region), cs = low(current.service_filter);\n const curWin = Number(current.time_window_days) || Number(cfg.time_window_days) || 30;\n const compat = candidates.filter(function (b) {\n if (str(b.run_stamp) === str(current.run_stamp)) return false; // not the current run\n if (cn && low(b.niche_id) && low(b.niche_id) !== cn) return false;\n if (cr && low(b.region) && low(b.region) !== cr) return false;\n if (cs && low(b.service_filter) && low(b.service_filter) !== cs) return false;\n if (b.data_mode && ['live', 'production'].indexOf(low(b.data_mode)) < 0) return false; // fixture/manual baselines excluded\n if (b.report_eligible === false) return false;\n if (Number(b.rows_after_filters) <= 0) return false; // empty snapshot is not a baseline\n if (b.time_window_days && Number(b.time_window_days) !== curWin) return false; // comparable window\n return true;\n });\n compat.sort(function (a, b) { return str(a.run_stamp) < str(b.run_stamp) ? -1 : 1; });\n const baseline = compat.length ? compat[compat.length - 1] : null;\n return {\n baseline: baseline,\n trend_status: baseline ? 'baseline_selected' : 'no_compatible_baseline',\n baseline_run_stamp: baseline ? str(baseline.run_stamp) : ''\n };\n}\n\n// Render a trend marker for a frequency vs an optional previous frequency. NEVER produce a dangling\n// \"(x34 =)\" \u2014 when there is no compatible baseline the marker is empty; an unchanged value renders no\n// bare \"=\" suffix. (Closure C1-D1 / S3-D7.)\nfunction trendMarker(freq, prevFreq, hasBaseline) {\n freq = Number(freq) || 0;\n if (!hasBaseline) return ''; // no baseline -> no suffix at all (not \"NEW\", not \"=\")\n if (prevFreq === undefined || prevFreq === null) return 'new';\n if (freq > prevFreq) return 'up';\n if (freq < prevFreq) return 'down';\n return 'flat'; // unchanged -> a word, never a bare \"=\"\n}\n\n// Compose the \"(xN)\" / \"(xN, up)\" suffix safely. Empty/whitespace markers never leave a dangling token.\nfunction freqSuffix(freq, marker) {\n freq = Number(freq) || 0;\n const m = str(marker);\n return m ? ('(x' + freq + ', ' + m + ')') : ('(x' + freq + ')');\n}\n// --- end embedded report_gate ---\n// WF10 v0.3 \u2014 DETERMINISTIC market intelligence aggregation (DEC-104; v0.2 patch DEC-106/107; v0.3 patch DEC-127). No LLM, no external calls, $0.\n// v0.2: no-data guard (rows_after_filters=0 -> marked no_data plan, no normal outputs); entity resolution\n// prefers company_name -> profile_url -> canonical listing id from source_url -> profile_name -> offer+platform.\n// WF08 answers \"what is this one record?\"; WF10 answers \"what is happening in the market overall?\".\n// Aggregate-only audience analysis (CONTACT_AND_OUTREACH_POLICY): no contacts invented, no outreach recommendations.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cut(s,n){s=str(s);if(!(s.length>n)||!(n>0))return s;var head=s.slice(0,Math.max(1,n-1));var c2=head.replace(/\\s+\\S*$/,'')||head;return c2.replace(/[\\s,;:.\\u2013\\u2014-]+$/,'')+'\\u2026';}\nfunction hashText(s){s=str(s);let h=5381;for(let i=0;i<s.length;i++){h=((h<<5)+h+s.charCodeAt(i))>>>0;}return ('00000000'+h.toString(16)).slice(-8);}\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction uniqPush(arr,v,max){v=str(v);if(!v)return;if(arr.indexOf(v)<0&&arr.length<(max||9999))arr.push(v);}\nconst cfg=$('Set Aggregator Config').first().json;\nconst now=moscowIsoNow();\nconst stamp=str(cfg.run_id).replace(/^wf10_/,'');\nconst windowDays=Number(cfg.time_window_days)||30;\nconst cutoffMs=Date.now()-windowDays*86400000;\nconst maxEx=Number(cfg.max_examples_per_item)||3;\nconst pf=low(cfg.platform_filter);\nconst stf=low(cfg.service_type_filter);\nconst rf=low(cfg.region_filter);\nfunction readTab(node,tab){let out=[];try{out=$(node).all().map(i=>i.json).filter(Boolean);}catch(e){out=[];}\n return out.filter(r=>Object.keys(r).some(k=>str(r[k])!=='' )).map(r=>Object.assign({_tab:tab},r));}\nlet rows=[].concat(readTab('Read monitor_queue','monitor_queue'),readTab('Read content_queue','content_queue'),readTab('Read review_queue','review_queue'));\nconst rows_considered=rows.length;\nfunction inWindow(r){const t=Date.parse(str(r.created_at)||str(r.parsed_at));if(isNaN(t))return false;return t>=cutoffMs;}\nrows=rows.filter(inWindow);\nconst rows_in_window=rows.length;\nfunction matches(r){\n // EXPLICIT-SCOPE-001: 'ANY' disables an INFERRED filter. When the user names a source outright\n // (\"\u043e\u0442\u0447\u0451\u0442 \u043f\u043e autolombardn1.ru\"), the named source IS the scope \u2014 an inferred region/service default must not\n // exclude it. Live: the site declared region \"\u0420\u043e\u0441\u0441\u0438\u044f\" while the plan defaulted to \"\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e\", so the region\n // filter dropped the very source the user asked about (rows_after_isolation=0, empty report). Run isolation\n // already scopes these rows to this request, so the extra inferred filter only ever subtracts.\n if(pf&&low(pf)!=='any'&&low(r.platform)!==pf)return false;\n if(stf&&low(stf)!=='any'&&low(r.service_type)!==stf)return false;\n if(rf&&low(rf)!=='any'){const reg=low(r.region);if(reg&®.indexOf(rf)<0&&rf.indexOf(reg)<0)return false;}\n return true;\n}\nrows=rows.filter(matches);\n// --- Stage C Closure Patch 2: run isolation + WF16 source_health enforcement ---\nconst __healthRows=readTab('Read source_health','source_health');\nconst __gate=buildEligibility(__healthRows, cfg);\nfunction __isoMatch(r){\n if(str(cfg.source_run_id_filter)&&!runFamilyMatch(str(r.source_run_id)||str(r.agent_request_id)||str(r.run_id),str(cfg.source_run_id_filter)))return false; // SCOPE-STRICT-001 + ISO-RUNID-001: WF04's queue rows carry the request family ONLY in run_id (req_x::source::slot); source_run_id/agent_request_id are empty, and runFamilyMatch('')===false, so isolation dropped every row of its OWN run.\n if(str(cfg.agent_request_id_filter)&&!runFamilyMatch(str(r.agent_request_id)||str(r.source_run_id)||str(r.run_id),str(cfg.agent_request_id_filter)))return false; // ISO-ARID-001: queue rows carry no agent_request_id column \u2014 isolate by request family via the source_run_id root (req_x::source::slot) instead of a strict equality that drops every row.\n if(str(cfg.data_mode_filter)&&str(r.data_mode)&&low(r.data_mode)!==low(cfg.data_mode_filter))return false; // ISO-RUNID-001: queue rows leave data_mode empty. ABSENT is not a MISMATCH (same guard the region filter already uses); a real 'fixture' row is still non-empty and still excluded, and run isolation above already scopes to this request.\n if(str(cfg.report_eligible_filter)!==''&&String(r.report_eligible)!==str(cfg.report_eligible_filter))return false;\n if(str(cfg.quality_status_filter)&&low(r.quality_status)!==low(cfg.quality_status_filter))return false;\n if(str(cfg.review_status_filter)&&low(r.review_status)!==low(cfg.review_status_filter))return false;\n return true;\n}\nrows=rows.filter(__isoMatch);\nconst rows_after_isolation=rows.length;\nlet rows_excluded_by_health=0; let rows_excluded_by_review=0; const __exclReasons={};\nif(cfg.enforce_source_health!==false){\n rows=rows.filter(function(r){\n const v=rowEligible(r,__gate,cfg);\n if(!v.eligible){\n if(/review|pending|deterministic_uncertain/.test(v.reason))rows_excluded_by_review++; else rows_excluded_by_health++;\n __exclReasons[v.reason]=(__exclReasons[v.reason]||0)+1; return false;\n }\n return true;\n });\n}\nconst rows_after_filters=rows.length;\n// --- Stage C Closure Patch 3: stamp source lineage onto aggregate outputs so WF12 can gate defensively ---\nfunction __lin(arr){var ids=[],dm={};for(var i=0;i<arr.length;i++){var rr=arr[i]||{};var id=str(rr.source_run_id);if(id&&ids.indexOf(id)<0)ids.push(id);var d=low(rr.data_mode)||'live';dm[d]=1;}var dks=Object.keys(dm);return {source_run_ids:ids.join(', '),data_mode:(dks.length===1?dks[0]:(dks.length===0?'live':'mixed')),report_eligible:true};}\n// ---- shared vocab (hardcoded credit_brokerage v0.1; NICHE_PACK_SYSTEM_PLAN will externalize this) ----\nconst KW=[['\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440','\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','\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','\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\u0449\u044c \u0441 \u043a\u0440\u0435\u0434\u0438\u0442','\u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043a\u0440\u0435\u0434\u0438\u0442\u043e\u043c'],['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438'],['\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b'],['\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442'],['\u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u0440\u0430\u0431\u043e\u0442\u0430 \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443'],['\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a','\u043f\u043e\u0434\u0431\u043e\u0440 \u0431\u0430\u043d\u043a\u0430'],['\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435','\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u0430'],['\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430','\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442'],['\u043e\u0431\u043e\u0440\u043e\u0442\u043d','\u043e\u0431\u043e\u0440\u043e\u0442\u043d\u044b\u0435 \u043a\u0440\u0435\u0434\u0438\u0442\u044b'],['\u0442\u0435\u043d\u0434\u0435\u0440\u043d','\u0442\u0435\u043d\u0434\u0435\u0440\u043d\u044b\u0435 \u0437\u0430\u0439\u043c\u044b'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a \u0433\u0430\u0440\u0430\u043d\u0442','\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0438\u0435 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438'],['\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440','\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435'],['\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a','\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a\u0438'],['\u0431\u044b\u0441\u0442\u0440\u043e','\u0431\u044b\u0441\u0442\u0440\u043e'],['\u0441\u0440\u043e\u0447\u043d\u043e','\u0441\u0440\u043e\u0447\u043d\u043e']];\nfunction extractKw(blob){const seen={};const out=[];for(const p of KW){if(blob.indexOf(p[0])>=0&&!seen[p[1]]){seen[p[1]]=1;out.push(p[1]);}}return out;}\n// v0.3 (DEC-127): pain labels merged for manager readability; trust pains (prepayment/fraud fear) added \u2014\n// these dominate VK audience comments and were previously invisible in top_pains.\nconst PAINS=[['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0438','\u043e\u0442\u043a\u0430\u0437\u044b \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0438 / \u043f\u043b\u043e\u0445\u0430\u044f \u041a\u0418'],['\u0441\u0440\u043e\u0447\u043d\u043e \u043d\u0443\u0436\u043d\u044b \u0434\u0435\u043d\u044c\u0433\u0438','\u0441\u0440\u043e\u0447\u043d\u0430\u044f \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u044c \u0432 \u0434\u0435\u043d\u044c\u0433\u0430\u0445'],['\u043d\u0435 \u0434\u0430\u044e\u0442 \u043a\u0440\u0435\u0434\u0438\u0442','\u043d\u0435 \u0434\u0430\u044e\u0442 \u043a\u0440\u0435\u0434\u0438\u0442'],['\u0431\u043e\u044e\u0441\u044c \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u0431\u0435\u0440\u0443\u0442 \u0434\u0435\u043d\u044c\u0433\u0438 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u0434\u0435\u043d\u044c\u0433\u0438 \u0437\u0430 \u043a\u0440\u0435\u0434\u0438\u0442 \u0438 \u043f\u0440\u043e\u043f\u0430\u0434\u0430','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432'],['\u043e\u0431\u043c\u0430\u043d','\u0441\u0442\u0440\u0430\u0445 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b / \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u043a\u043e\u0432']];\nfunction extractPains(blob){const seen={};const out=[];for(const p of PAINS){if(blob.indexOf(p[0])>=0&&!seen[p[1]]){seen[p[1]]=1;out.push(p[1]);}}return out;}\nfunction rowBlob(r){return low(str(r.offer_text)+' '+str(r.terms)+' '+str(r.reason)+' '+str(r.detected_need)+' '+cut(str(r.text_context),900)+' '+cut(str(r.comment_text),900));}\nfunction listingId(u){const m=str(u).match(/_(\\d{6,})(?:[\\/?#]|$)/)||str(u).match(/\\/(\\d{7,})(?:[\\/?#]|$)/);return m?m[1]:'';}\nfunction normName(s){return low(s).replace(/[^a-z\u0430-\u044f\u04510-9]+/gi,' ').replace(/\\s+/g,' ').trim();}\nfunction adChannel(r){const p=low(r.platform);if(p==='avito'||low(r.source_type)==='classified')return 'classifieds';if(p)return p;return 'unknown';}\n// ---- 1. competitor_profiles (group competitor records) ----\nconst compRows=rows.filter(r=>low(r.entity_type)==='competitor'||r._tab==='monitor_queue');\nfunction normProfileUrl(u){u=low(u);if(!u)return '';u=u.replace(/[?#].*$/,'').replace(/^https?:\\/\\//,'').replace(/^www\\./,'').replace(/\\/+$/,'');return u;}\n// v0.2 entity resolution (DEC-107): named identity first, then stable URL identities (profile_url, canonical\n// Avito/classified listing id from source_url), then profile_name, and normalized offer_text+platform ONLY as\n// a last fallback before the record hash \u2014 so repeated unnamed rows of the same listing form ONE profile.\nfunction groupKey(r){\n const cn=normName(r.company_name);if(cn)return 'name::'+cn;\n const pu=normProfileUrl(r.profile_url);if(pu)return 'purl::'+pu;\n const lid=listingId(r.source_url);if(lid)return 'listing::'+low(r.platform)+'::'+lid;\n const pn=normName(r.profile_name);if(pn)return 'profile::'+pn;\n const ot=normName(cut(r.offer_text,80));if(ot)return 'offer::'+low(r.platform)+'::'+ot;\n return 'rec::'+hashText(low(str(r.source_url)+str(r.offer_text)));\n}\nconst groups={};\nfor(const r of compRows){const k=groupKey(r);(groups[k]=groups[k]||[]).push(r);}\nconst competitor_profiles=[];\nfor(const k of Object.keys(groups)){\n const g=groups[k];\n const dates=g.map(r=>Date.parse(str(r.created_at))).filter(t=>!isNaN(t)).sort((a,b)=>a-b);\n function iso(t){var m=new Date(t+10800000);return m.toISOString().replace('Z','+03:00');}\n const platforms=[],urls=[],services=[],offers=[],terms=[],channels=[];let kws=[],pains=[];let strengthSum=0,strengthN=0;\n for(const r of g){\n uniqPush(platforms,low(r.platform));uniqPush(urls,r.source_url,maxEx);uniqPush(services,low(r.service_type));\n uniqPush(offers,cut(r.offer_text,140),maxEx);uniqPush(terms,cut(r.terms,140),maxEx);uniqPush(channels,adChannel(r));\n const blob=rowBlob(r);for(const w of extractKw(blob))uniqPush(kws,w);for(const w of extractPains(blob))uniqPush(pains,w);\n const cs=Number(r.competitor_strength);if(cs>1){strengthSum+=cs;strengthN++;}\n }\n const name=str(g[0].company_name)||str(g[0].profile_name)||('(unnamed) '+cut(str(g[0].offer_text),60));\n const evid=g.length;\n const hasPriced=g.some(r=>str(r.terms)!=='');\n const conf=hasPriced?80:(evid>=2?60:45);\n const avgStrength=strengthN?Math.round(strengthSum/strengthN):0;\n const strengths=[];\n if(hasPriced)strengths.push('\u044f\u0432\u043d\u044b\u0439 \u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c ('+terms.join(' / ')+')');\n if(kws.indexOf('\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442')>=0)strengths.push('\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442');\n if(kws.indexOf('\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b')>=0)strengths.push('\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b');\n if(avgStrength>=75)strengths.push('\u0441\u0438\u043b\u044c\u043d\u043e\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 (\u0441\u0440\u0435\u0434\u043d\u044f\u044f \u043e\u0446\u0435\u043d\u043a\u0430 \u0437\u0430\u043c\u0435\u0442\u043d\u043e\u0441\u0442\u0438 '+avgStrength+')');\n const weaknesses=[];\n if(!hasPriced)weaknesses.push('\u0446\u0435\u043d\u0430/\u0443\u0441\u043b\u043e\u0432\u0438\u044f \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b');\n if(platforms.length===1)weaknesses.push('\u0432\u0438\u0434\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043a\u0430\u043d\u0430\u043b ('+platforms.join('')+')');\n var __lc=__lin(g);\n competitor_profiles.push({\n competitor_id:'comp_'+hashText(k),\n source_run_ids:__lc.source_run_ids, data_mode:__lc.data_mode, report_eligible:__lc.report_eligible,\n competitor_name:name,\n first_seen_at:dates.length?iso(dates[0]):'',\n last_seen_at:dates.length?iso(dates[dates.length-1]):'',\n platforms:platforms.join(', '),\n source_urls:urls.join(' | '),\n service_types:services.join(', '),\n offers:offers.join(' | '),\n prices_terms:terms.join(' | '),\n semantic_keywords:kws.slice(0,12).join(', '),\n pain_points_targeted:pains.join(', '),\n ad_channels:channels.join(', '),\n observed_strengths:strengths.join('; '),\n observed_weaknesses:weaknesses.join('; '),\n evidence_count:evid,\n source_confidence_score:conf,\n notes:'wf10 v0.3 deterministic; run '+str(cfg.run_id)+'; window '+windowDays+'d; group_key='+cut(k,60)\n });\n}\n// ---- 2. market_angles ----\nconst ANGLES=[\n ['speed','\u0441\u043a\u043e\u0440\u043e\u0441\u0442\u044c / \u0441\u0440\u043e\u0447\u043d\u043e\u0441\u0442\u044c (\u00ab\u0431\u044b\u0441\u0442\u0440\u043e\u00bb, \u00ab\u0437\u0430 1 \u0434\u0435\u043d\u044c\u00bb)','speed',['\u0431\u044b\u0441\u0442\u0440\u043e','\u0441\u0440\u043e\u0447\u043d\u043e','\u0437\u0430 1 \u0434\u0435\u043d\u044c','\u0432 \u0434\u0435\u043d\u044c \u043e\u0431\u0440\u0430\u0449\u0435\u043d\u0438\u044f'],'\u043a\u043b\u0438\u0435\u043d\u0442\u0443 \u043d\u0443\u0436\u043d\u043e \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043d\u0435\u043c\u0435\u0434\u043b\u0435\u043d\u043d\u043e','\u041f\u043e\u0441\u0442 \u00ab\u0420\u0435\u0430\u043b\u044c\u043d\u044b\u0435 \u0441\u0440\u043e\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u0430: \u043e\u0442 \u0447\u0435\u0433\u043e \u043e\u043d\u0438 \u0437\u0430\u0432\u0438\u0441\u044f\u0442\u00bb + \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u0440\u0430\u0437\u0431\u043e\u0440 \u044d\u0442\u0430\u043f\u043e\u0432'],\n ['price_anchor','\u0446\u0435\u043d\u043e\u0432\u043e\u0439 \u044f\u043a\u043e\u0440\u044c (\u00ab\u043e\u0442 N \u20bd\u00bb)','price',['__PRICE__'],'\u0441\u0442\u0440\u0430\u0445 \u0441\u043a\u0440\u044b\u0442\u044b\u0445 \u043a\u043e\u043c\u0438\u0441\u0441\u0438\u0439','\u041f\u0440\u043e\u0437\u0440\u0430\u0447\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0446\u0435\u043d + \u043f\u043e\u0441\u0442 \u00ab\u0418\u0437 \u0447\u0435\u0433\u043e \u0441\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043c\u0438\u0441\u0441\u0438\u044f \u0431\u0440\u043e\u043a\u0435\u0440\u0430\u00bb'],\n ['no_prepayment','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','trust',['\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b'],'\u0441\u0442\u0440\u0430\u0445 \u043c\u043e\u0448\u0435\u043d\u043d\u0438\u0447\u0435\u0441\u0442\u0432\u0430/\u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u041f\u043e\u0441\u0442 \u00ab\u041f\u043e\u0447\u0435\u043c\u0443 \u0447\u0435\u0441\u0442\u043d\u044b\u0439 \u0431\u0440\u043e\u043a\u0435\u0440 \u043c\u043e\u0436\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b\u00bb + \u0434\u043e\u0433\u043e\u0432\u043e\u0440-\u043e\u0444\u0435\u0440\u0442\u0430'],\n ['result_payment','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
10 - Competitor Audience Intelligence Aggregator. Uses googleSheets, executeWorkflowTrigger. Event-driven trigger; 24 nodes.
Source: https://github.com/CodeVinci8/vinci-ai-pilot/blob/main/n8n/workflows/10_competitor_audience_intelligence_aggregator.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.
12 - Market Intelligence Report Builder (Stakeholder v0.3). Uses googleSheets, httpRequest, executeWorkflowTrigger. Event-driven trigger; 27 nodes.
Google Maps Email Scraper Template. Uses removeDuplicates, splitInBatches, httpRequest, splitOut. Event-driven trigger; 26 nodes.
Splitout Comparedatasets. Uses manualTrigger, gong, stickyNote, executeWorkflow. Event-driven trigger; 26 nodes.
Streamline your sales call analysis with CallForge, an automated workflow that extracts, enriches, and refines Gong.io call data for AI-driven insights.
Email Scapper. Uses httpRequest, googleSheets, executeWorkflowTrigger. Event-driven trigger; 26 nodes.