This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "12 - Market Intelligence Report Builder (Stakeholder v0.3)",
"nodes": [
{
"id": "cc120000-0000-0000-0000-0000000000n1",
"name": "Overview Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-560,
-360
],
"parameters": {
"height": 760,
"width": 640,
"content": "## 12 \u2014 Market Intelligence Report Builder v0.1 \u2014 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u042b\u0419 \u0441\u043a\u0435\u043b\u0435\u0442 (DEC-112/118)\n\n\u041e\u0442\u0447\u0451\u0442\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 \u041d\u0410\u0414 \u0444\u0430\u043a\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u044f\u0434\u0440\u043e\u043c WF10 (DEC-112: Claude \u2014 \u0432 report/control-\u0441\u043b\u043e\u0435, \u041d\u0415 \u0432 WF10).\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):\n1. \u0420\u0443\u0447\u043d\u043e\u0439 \u0441\u0442\u0430\u0440\u0442 \u2192 Set Report Config (`enable_llm_summary=false`, `telegram_send=false`).\n2. \u0427\u0438\u0442\u0430\u0435\u0442 4 \u0432\u043a\u043b\u0430\u0434\u043a\u0438 WF10: competitor_profiles / market_angles / audience_activity_signals / content_positioning_plan.\n3. \u0412\u044b\u0431\u0438\u0440\u0430\u0435\u0442 \u041f\u041e\u0421\u041b\u0415\u0414\u041d\u0418\u0419 \u0441\u043d\u0430\u043f\u0448\u043e\u0442 WF10 \u043f\u043e `plan_id` (\u043e\u0434\u0438\u043d \u043f\u043b\u0430\u043d \u043d\u0430 \u043f\u0440\u043e\u0433\u043e\u043d); \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u043f\u043b\u0430\u043d = \u0431\u0430\u0437\u0430 \u0434\u043b\u044f \u0442\u0440\u0435\u043d\u0434\u043e\u0432 \u0443\u0433\u043b\u043e\u0432 (\u2191/\u2193/=/NEW).\n4. \u0421\u0447\u0438\u0442\u0430\u0435\u0442 \u0442\u043e\u043f \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (evidence_count/confidence), \u0442\u043e\u043f \u0443\u0433\u043b\u043e\u0432 (frequency+\u0442\u0440\u0435\u043d\u0434), \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b \u0430\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u0438; \u0440\u0435\u043d\u0434\u0435\u0440\u0438\u0442 Markdown.\n5. \u041f\u0438\u0448\u0435\u0442 \u0422\u041e\u041b\u042c\u041a\u041e: market_intelligence_reports (25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3, \u0441\u0445\u0435\u043c\u0430 \u0432 MARKET_INTELLIGENCE_REPORT_SCHEMA.md) + agent_requests (21, completed, $0).\n6. no_data-\u043f\u0440\u043e\u0433\u043e\u043d WF10 \u2192 report_type=no_data_notice, \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u044f, \u0431\u0435\u0437 \u0448\u0430\u0431\u043b\u043e\u043d\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430.\n7. **source_mix \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0432 \u043a\u0430\u0436\u0434\u043e\u043c \u043e\u0442\u0447\u0451\u0442\u0435** (DEC-108): \u043e\u0442\u0447\u0451\u0442 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u043f\u043e\u0434\u0440\u0430\u0437\u0443\u043c\u0435\u0432\u0430\u0435\u0442, \u0447\u0442\u043e \u0432\u0441\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441\u043e\u0431\u0440\u0430\u043d\u044b live.\n\n\u0427\u0415\u0413\u041e \u041d\u0415 \u0414\u0415\u041b\u0410\u0415\u0422: \u041d\u0415 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Claude (\u0432\u0435\u0442\u043a\u0430 = guard \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439; \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0435 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435: bounded facts-only prompt, \u0442\u043e\u043a\u0435\u043d-\u043a\u044d\u043f, \u0437\u0430\u043f\u0438\u0441\u044c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438); \u041d\u0415 \u0448\u043b\u0451\u0442 \u0432 Telegram (delivered_to=none); \u041d\u0415 \u0445\u043e\u0434\u0438\u0442 \u0432 \u0441\u0435\u0442\u044c (HTTP-\u043d\u043e\u0434 \u043d\u0435\u0442); \u041d\u0415 \u0432\u044b\u0434\u0443\u043c\u044b\u0432\u0430\u0435\u0442 \u0444\u0430\u043a\u0442\u044b/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b; \u041d\u0415 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442 outreach. active=false.\n\n**v0.2 (DEC-122):** \u043e\u0442\u0447\u0451\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d \u0441\u0435\u043a\u0446\u0438\u044f\u043c\u0438 executive_summary / competitor_snapshot / top_offers_and_prices / source_confidence / limitations / next_actions (\u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e, $0). \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u041e\u0425\u0420\u0410\u041d\u042f\u0415\u041c\u0410\u042f Claude-\u0432\u0435\u0442\u043a\u0430: \u0433\u0435\u0439\u0442 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f (token) -> evidence-bound \u043f\u0440\u043e\u043c\u043f\u0442 (\u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u043b\u044f \u043e\u0442\u0447\u0451\u0442\u0430) -> \u041e\u0422\u041a\u041b\u042e\u0427\u0401\u041d\u041d\u042b\u0419 HTTP-\u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440 (claude-sonnet-4-6, \u0431\u0435\u0437 \u043a\u043b\u044e\u0447\u0430) -> merge \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 ($3/$15 \u0437\u0430 MTok). WF10 = \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0444\u0430\u043a\u0442\u043e\u0432\u043e\u0435 \u044f\u0434\u0440\u043e; Claude = \u0441\u043b\u043e\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u0438\u043d\u0442\u0435\u0440\u043f\u0440\u0435\u0442\u0430\u0446\u0438\u0438 (DEC-112). \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u0432\u044b\u0434\u0443\u043c\u0430\u043d\u043d\u044b\u0445 \u0444\u0430\u043a\u0442\u043e\u0432/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432/\u0430\u0443\u0442\u0440\u0438\u0447\u0430.\n\n**v0.3 (DEC-128/129/130):** 1) \u0434\u0430\u0439\u0434\u0436\u0435\u0441\u0442 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f (5\u20137 \u0447\u0438\u0441\u0442\u044b\u0445 \u043f\u0443\u043d\u043a\u0442\u043e\u0432); 2) \u0447\u0438\u0441\u0442\u044b\u0435 \u0438\u043c\u0435\u043d\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (\u0432\u043c\u0435\u0441\u0442\u043e \u00ab(unnamed) \u2026\u00bb \u2014 \u00ab<Platform> offer: <\u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043e\u0444\u0444\u0435\u0440>\u00bb); 3) \u043e\u0444\u0444\u0435\u0440\u044b \u0443\u043a\u043e\u0440\u043e\u0447\u0435\u043d\u044b (max_offer_chars=90); 4) \u0431\u043b\u043e\u043a \u0441\u0430\u0439\u0442\u043e\u0432 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438\u0437 competitor_site_snapshots (Stage 2 reintegration); 5) \u0431\u043b\u043e\u043a \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 \u0438\u0437 public_lead_signals (\u0442\u043e\u043b\u044c\u043a\u043e \u0430\u0433\u0440\u0435\u0433\u0430\u0442\u044b, \u0431\u0435\u0437 \u0438\u043c\u0451\u043d/\u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432); 6) \u0431\u043b\u043e\u043a\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439: \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440 / \u043a\u043e\u043d\u0442\u0435\u043d\u0442 / \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438; 7) \u0441\u0445\u0435\u043c\u0430 \u043e\u0442\u0447\u0451\u0442\u0430 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0430 \u0434\u043e 25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a (llm_status/llm_model/llm_input_tokens/llm_output_tokens/llm_summary_ru/llm_recommendations_ru/llm_quality_flags); 8) Claude-\u0432\u0435\u0442\u043a\u0430: \u0431\u044e\u0434\u0436\u0435\u0442\u043d\u044b\u0439 \u0433\u0435\u0439\u0442 (llm_max_input_chars, llm_max_estimated_cost_usd \u2014 \u0431\u0440\u043e\u0441\u0430\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0414\u041e HTTP), JSON-\u0441\u0435\u043a\u0446\u0438\u0438 (executive_summary_ru/key_findings/market_risks/recommended_next_actions/content_recommendations/source_limitations), quality-\u0444\u043b\u0430\u0433\u0438 (non_json/truncated/unverified_numbers/outreach_language); 9) \u043a\u0430\u0436\u0434\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u043f\u0438\u0448\u0435\u0442 1 \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 live_source_runs."
}
},
{
"id": "cc120000-0000-0000-0000-0000000000n2",
"name": "Setup and Test Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
120,
-360
],
"parameters": {
"height": 720,
"width": 620,
"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). \u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u043a\u043b\u0430\u0434\u043a\u0443 `market_intelligence_reports` (25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a v0.3 \u2014 \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438\u0437 docs/MARKET_INTELLIGENCE_REPORT_SCHEMA.md).\n2. \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 6 sheet-\u043d\u043e\u0434\u0430\u0445 (4 read + 2 append) \u0438 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 ID.\n3. \u0422\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u0435: \u22651 \u043f\u0440\u043e\u0433\u043e\u043d WF10 (\u0438\u043d\u0430\u0447\u0435 \u043f\u043e\u043d\u044f\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u00abcontent_positioning_plan is empty\u00bb). \u0422\u0440\u0435\u043d\u0434\u044b \u0443\u0433\u043b\u043e\u0432 \u043f\u043e\u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u043f\u0440\u0438 \u22652 \u043f\u0440\u043e\u0433\u043e\u043d\u0430\u0445 WF10.\n4. \u0422\u0435\u0441\u0442 1 ($0): Execute once \u2192 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e: market_intelligence_reports +1 (report_type=on_demand; top_competitors/top_angles \u0438\u0437 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0430 WF10; llm_* \u043f\u0443\u0441\u0442\u044b\u0435; llm_cost_usd=0; delivered_to=none; source_mix \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d), agent_requests +1 (request_type=report_summary, completed, $0).\n5. \u0422\u0435\u0441\u0442 2 (no-data): \u0435\u0441\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043e\u043d WF10 \u0431\u044b\u043b no_data \u2192 report_type=no_data_notice, \u0442\u043e\u043f-\u0441\u043f\u0438\u0441\u043a\u0438 \u043f\u0443\u0441\u0442\u044b\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0430\u0446\u0438\u044f \u00abno_data; broaden filters or source scope\u00bb.\n6. \u0422\u0435\u0441\u0442 3 (LLM guard): enable_llm_summary=true \u2192 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043d\u0430 \u043d\u043e\u0434\u0435 LLM Summary Guard. \u0412\u0435\u0440\u043d\u0443\u0442\u044c false.\n\nTelegram-\u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u0438 Claude-summary \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0435 \u0448\u0430\u0433\u0438 \u0437\u0430 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435\u043c (REPORTING_AND_TELEGRAM_SUMMARY_PLAN.md \u00a75).\n\n**v0.3:** \u0432\u043a\u043b\u0430\u0434\u043a\u0430 market_intelligence_reports \u0442\u0435\u043f\u0435\u0440\u044c 25 \u043a\u043e\u043b\u043e\u043d\u043e\u043a (\u0441\u043c. MARKET_INTELLIGENCE_REPORT_SCHEMA.md v0.3 \u2014 \u043f\u0435\u0440\u0435\u0441\u043e\u0437\u0434\u0430\u0442\u044c/\u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438). \u0421\u043e\u0437\u0434\u0430\u0442\u044c \u0432\u043a\u043b\u0430\u0434\u043a\u0438 competitor_site_snapshots (22) / public_lead_signals (47, v0.3) / live_source_runs (23) \u0438\u0437 docs/TABLE_SCHEMA.md (\u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c\u0438 \u2014 \u043e\u0442\u0447\u0451\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0431\u0435\u0437 \u043d\u0438\u0445). 8 sheet-\u043d\u043e\u0434 (6 read + 3 append... \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u043d\u0430 \u0432\u0441\u0435\u0445). \u0422\u0435\u0441\u0442 Claude (\u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e): llm_approval_token=I_APPROVE_CLAUDE_REPORT_SUMMARY + enable_llm_summary=true + Anthropic \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b \u0432 n8n + \u0412\u041a\u041b\u042e\u0427\u0418\u0422\u042c disabled HTTP-\u043d\u043e\u0434\u0443; \u0431\u044e\u0434\u0436\u0435\u0442-\u0442\u0435\u0441\u0442: llm_max_estimated_cost_usd=0.0001 -> \u043e\u0448\u0438\u0431\u043a\u0430 budget guard \u0414\u041e HTTP."
}
},
{
"id": "cc120000-0000-0000-0000-0000000000m1",
"name": "Manual Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-440,
320
],
"parameters": {}
},
{
"id": "cc120000-0000-0000-0000-0000000000c1",
"name": "Set Report Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-220,
320
],
"parameters": {
"jsCode": "// WF12 Market Intelligence Report Builder config \u2014 v0.3 stakeholder report (DEC-112/118/122/128).\n// $0 by default: no Claude, no Telegram send, no external API. Reads WF10 tabs + competitor_site_snapshots\n// + public_lead_signals; writes ONE market_intelligence_reports row (25 cols, MARKET_INTELLIGENCE_REPORT_SCHEMA.md v0.3),\n// ONE agent_requests row and ONE live_source_runs row.\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();\nreturn [{ json: {\n // --- hard-off defaults (DEC-112): the deterministic report is complete without any of these.\n enable_llm_summary:false, // optional Claude summary branch \u2014 guarded, HTTP node DISABLED\n llm_approval_token:'', // must equal 'I_APPROVE_CLAUDE_REPORT_SUMMARY' (explicit operator approval)\n llm_model:'claude-sonnet-4-6', // project stack model (CLAUDE.md); pricing for cost tracking below\n llm_max_tokens:1200, // hard output cap for the summary\n llm_max_input_chars:8000, // budget guard (DEC-128): prompt facts hard-capped at this size\n llm_max_estimated_cost_usd:0.10, // budget guard (DEC-128): throw BEFORE the HTTP node if estimate exceeds\n llm_input_price_per_mtok:3.0, // USD per 1M input tokens (claude-sonnet-4-6)\n llm_output_price_per_mtok:15.0, // USD per 1M output tokens (claude-sonnet-4-6)\n telegram_send:false, // Telegram delivery \u2014 NOT implemented (delivered_to stays 'none')\n report_type:'on_demand', // on_demand / weekly_digest / no_data_notice (auto-set on no_data)\n niche_id:'credit_brokerage',\n region:'\u041c\u043e\u0441\u043a\u0432\u0430/\u041c\u041e',\n top_n:3,\n max_offer_chars:90, // v0.3: long competitor offers are shortened for readability\n requested_by:'operator',\n // --- Stage C Closure Patch 2: WF16 source_health enforcement + explicit fixture-report mode (\u00a711.5/\u00a711.7) ---\n enforce_source_health:true,\n require_source_health:false,\n allow_degraded_report:false, // explicit opt-in to include degraded runs (renders a visible warning)\n allow_fixture_report:false, // explicit opt-in to generate a fixture/manual_test report (renders watermark)\n report_data_mode:'live', // live | fixture | manual_test\n allow_unverified_source:false, // PRODUCTION fail-closed (see WF10); set true ONLY for non-production/dev runs\n report_id:'report_'+stamp,\n run_id:'wf12_'+stamp,\n agent_request_id:'wf12_req_'+stamp,\n created_at:moscowIsoNow()\n}}];"
}
},
{
"id": "cc120000-0000-0000-0000-0000000000r1",
"name": "Read competitor_profiles",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
0,
320
],
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID_HERE",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "competitor_profiles",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"id": "cc120000-0000-0000-0000-0000000000r2",
"name": "Read market_angles",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
220,
320
],
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID_HERE",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "market_angles",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"id": "cc120000-0000-0000-0000-0000000000r3",
"name": "Read audience_activity_signals",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
440,
320
],
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID_HERE",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "audience_activity_signals",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"id": "cc120000-0000-0000-0000-0000000000r4",
"name": "Read content_positioning_plan",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
660,
320
],
"parameters": {
"operation": "read",
"documentId": {
"__rl": true,
"value": "PASTE_SPREADSHEET_ID_HERE",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "content_positioning_plan",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"id": "cc120000-0000-0000-0000-0000000000b1",
"name": "Build Deterministic Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
320
],
"parameters": {
"jsCode": "// --- 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\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 if (review_status === 'pending' || /(^|; )pending_review/.test(flags)) 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 const id = str(row.source_run_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 const selfAttestsLive = (dm === 'live' || dm === 'production') &&\n (qs === '' || qs === 'healthy' || (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 || 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// Deterministic Report Builder core v0.3 (DEC-112/118/122/128): latest WF10 snapshot + website snapshots\n// + public lead signals -> 25-col report row + stakeholder Markdown. Facts only \u2014 every number/name comes\n// from sheet rows; nothing is invented; no contacts are surfaced; no outreach recommendations.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction num(v){var n=Number(v);return isFinite(n)?n:0;}\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;}\n// Defect A (C6): deterministic report-layer contact redaction. Removes direct contact identifiers that may be\n// embedded in lead evidence text (phones, @handles, t.me/VK profile links, emails) BEFORE truncation/formatting,\n// while preserving business numbers (amounts, %, rates, dates) and ordinary source-POST URLs (vk.com/wall...).\nconst CONTACT_MARK='[PUBLIC CONTACT REDACTED]';\nfunction redact(s){ s=str(s); if(!s)return s;\n s=s.replace(/[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}/g,CONTACT_MARK); // emails (before @handle)\n s=s.replace(/(?:\\+7|8)[\\s\\-()]*\\d{3}[\\s\\-()]*\\d{3}[\\s\\-]*\\d{2}[\\s\\-]*\\d{2}/g,CONTACT_MARK); // RU phones\n s=s.replace(/\\+\\d[\\d\\s().\\-]{8,}\\d/g,CONTACT_MARK); // intl phone-like\n s=s.replace(/(?:https?:\\/\\/)?(?:t|telegram)\\.me\\/[A-Za-z0-9_+]+/gi,CONTACT_MARK); // telegram links\n s=s.replace(/@[A-Za-z0-9_]{3,32}/g,CONTACT_MARK); // @handles\n s=s.replace(/(?:https?:\\/\\/)?(?:www\\.)?vk\\.com\\/(?!wall)[A-Za-z0-9_.]+/gi,CONTACT_MARK); // VK profiles/groups (keep wall posts)\n return s;\n}\nfunction rowsOf(name){try{return $(name).all().map(i=>i.json).filter(r=>r&&Object.keys(r).some(k=>str(r[k])!==''));}catch(e){return [];}}\nconst cfg=$('Set Report Config').first().json;\nconst topN=Number(cfg.top_n)||3;\nconst maxOffer=Number(cfg.max_offer_chars)||90;\nconst profiles=rowsOf('Read competitor_profiles');\nconst angles=rowsOf('Read market_angles');\nconst signals=rowsOf('Read audience_activity_signals');\nconst plans=rowsOf('Read content_positioning_plan').filter(p=>str(p.plan_id));\nconst siteSnaps=rowsOf('Read competitor_site_snapshots');\nconst leadSignals=rowsOf('Read public_lead_signals');\n// --- Stage C Closure Patch 2: WF16 source_health enforcement (\u00a711.5) ---\nconst healthRows=rowsOf('Read source_health');\nconst __gate=buildEligibility(healthRows, cfg);\nconst included_source_runs=__gate.eligible_run_ids;\nconst excluded_source_runs=__gate.excluded_run_ids;\nconst report_contains_degraded_data=__gate.has_degraded_included;\nconst report_contains_fixture_data=(cfg.allow_fixture_report===true)&&__gate.has_fixture_warning;\nconst isFixtureReport=(cfg.allow_fixture_report===true)||['fixture','manual_test'].indexOf(low(cfg.report_data_mode))>=0||report_contains_fixture_data;\nconst quality_warning=(report_contains_degraded_data?'DEGRADED source data included by explicit opt-in \u2014 verify before acting. ':'')+(isFixtureReport?'TEST/FIXTURE data \u2014 not production intelligence. ':'');\n// --- run selection: plan_id carries the WF10 run stamp (plan_<stamp> or plan_<stamp>_no_data; one plan per run)\nfunction planStamp(p){return str(p.plan_id).replace(/^plan_/,'').replace(/_no_data$/,'');}\nplans.sort((a,b)=>planStamp(a)<planStamp(b)?-1:1);\nif(!plans.length){throw new Error('WF12: content_positioning_plan is empty \u2014 run WF10 at least once before building a report.');}\nconst latest=plans[plans.length-1];\nconst stamp=planStamp(latest);\n// --- Stage C Closure Patch 2: compatible trend baseline (\u00a711.6, S3-D7) \u2014 never mark all NEW on no baseline.\nconst __baselineCandidates=plans.filter(p=>planStamp(p)!==stamp).map(function(p){var rm=str(p.source_evidence).match(/rows=(\\d+)/);var rows=rm?Number(rm[1]):0;var nd=/_no_data$/.test(str(p.plan_id))||rows===0;return {run_stamp:planStamp(p),niche_id:p.niche,region:p.region,service_filter:'',data_mode:nd?'no_data':'live',report_eligible:!nd,rows_after_filters:rows,time_window_days:30};});\nconst __bl=selectBaseline(__baselineCandidates,{run_stamp:stamp,niche_id:str(cfg.niche_id),region:str(cfg.region),time_window_days:30},{});\nconst trend_status=__bl.trend_status;\nconst hasBaseline=!!__bl.baseline;\nconst prevStamp=__bl.baseline_run_stamp;\nconst isNoData=/_no_data$/.test(str(latest.plan_id))||/rows=0/.test(str(latest.source_evidence));\nconst rowsAfter=(function(){const m=str(latest.source_evidence).match(/rows=(\\d+)/);return m?Number(m[1]):0;})();\n// --- snapshot filters: WF10 ids/notes carry the run stamp\nlet profNow=profiles.filter(p=>str(p.notes).indexOf('wf10_'+stamp)>=0||str(p.notes).indexOf(stamp)>=0);\nlet angNow=angles.filter(a=>str(a.angle_id).endsWith(stamp));\nconst angPrev=prevStamp?angles.filter(a=>str(a.angle_id).endsWith(prevStamp)):[];\nlet sigNow=signals.filter(s=>str(s.signal_id).endsWith(stamp));\n// --- Stage C Closure Patch 3: defensive body gate. The rendered body must contain ONLY records whose\n// stamped source lineage is eligible under the CURRENT source_health (Obj1.10/1.11). A WF10 snapshot\n// row built from a now-quarantined / fixture / report_ineligible source run is dropped from the body,\n// keeping the body and the source-quality section consistent.\nfunction __bodyEligible(row){\n var ids=String(row.source_run_ids||row.source_run_id||'').split(/\\s*,\\s*/).filter(Boolean);\n if(ids.length===0) return rowEligible(row,__gate,cfg).eligible;\n for(var i=0;i<ids.length;i++){ if(!rowEligible({source_run_id:ids[i],data_mode:row.data_mode,report_eligible:row.report_eligible,quality_status:row.quality_status,review_status:row.review_status},__gate,cfg).eligible) return false; }\n return true;\n}\nconst __profBefore=profNow.length, __angBefore=angNow.length, __sigBefore=sigNow.length;\nprofNow=profNow.filter(__bodyEligible); angNow=angNow.filter(__bodyEligible); sigNow=sigNow.filter(__bodyEligible);\nconst body_records_excluded=(__profBefore-profNow.length)+(__angBefore-angNow.length)+(__sigBefore-sigNow.length);\n// --- v0.3 clean competitor names (DEC-128): never show ugly '(unnamed) <long offer>' to a stakeholder.\n// Fallback: '<Platform> offer: <short offer text>'.\nfunction capWord(s){s=str(s);return s?(s.charAt(0).toUpperCase()+s.slice(1)):s;}\nfunction cleanName(p){\n const n=str(p.competitor_name);\n if(n&&n.indexOf('(unnamed)')!==0)return cut(n,60);\n const plat=capWord(str(p.platforms).split(/\\s*,\\s*/)[0]||'market');\n const offer=cut(str(p.offers).split(' | ')[0]||n.replace(/^\\(unnamed\\)\\s*/,''),60);\n return plat+' offer: '+(offer||'(no offer text)');\n}\n// --- deterministic top lists\nconst topCompRows=profNow.slice().sort((a,b)=>(num(b.evidence_count)-num(a.evidence_count))||(num(b.source_confidence_score)-num(a.source_confidence_score))).slice(0,topN);\nconst topComp=topCompRows.map(p=>cleanName(p)+' (evidence '+num(p.evidence_count)+', conf '+num(p.source_confidence_score)+')');\nfunction angleKey(a){return str(a.angle_id).replace(/^angle_/,'').replace(/_\\d{8}_\\d{6}$/,'');}\nconst prevFreq={};for(const a of angPrev){prevFreq[angleKey(a)]=num(a.frequency);}\nconst topAng=angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency)).slice(0,topN)\n .map(a=>{const k=angleKey(a);const f=num(a.frequency);\n const marker=trendMarker(f, hasBaseline?prevFreq[k]:undefined, hasBaseline);\n return str(a.angle_text)+' '+freqSuffix(f, marker);});\nconst audSummary=sigNow.map(s=>str(s.platform)+': questions='+num(s.question_count)+', objections='+num(s.objection_count)+', complaints='+num(s.complaint_count)+', buying_intent='+num(s.buying_intent_count)+(str(s.top_pains)?(', pains: '+str(s.top_pains)):'')).join(' | ');\nconst NO_DATA_TEXT='no_data; broaden filters or source scope';\n// --- v0.3 shortened offers/prices\nconst offersPrices=profNow.filter(p=>str(p.offers)||str(p.prices_terms)).slice(0,5)\n .map(p=>cleanName(p)+': '+cut(str(p.offers).split(' | ')[0]||'(\u043e\u0444\u0444\u0435\u0440 \u043d\u0435 \u0438\u0437\u0432\u043b\u0435\u0447\u0451\u043d)',maxOffer)+(str(p.prices_terms)?(' \u2014 '+cut(str(p.prices_terms).split(' | ')[0],maxOffer)):''));\nconst confScores=profNow.map(p=>num(p.source_confidence_score)).filter(n=>n>0);\nconst confAvg=confScores.length?Math.round(confScores.reduce((a,b)=>a+b,0)/confScores.length):0;\nconst confLine=confScores.length?('avg='+confAvg+', min='+Math.min.apply(null,confScores)+', max='+Math.max.apply(null,confScores)+' (\u043f\u043e '+confScores.length+' \u043f\u0440\u043e\u0444\u0438\u043b\u044f\u043c; \u043f\u0440\u0430\u0432\u0438\u043b\u0430: source_confidence_rules)'):'(\u043d\u0435\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435)';\nconst platformsSeen={};profNow.forEach(p=>str(p.platforms).split(/\\s*,\\s*/).forEach(x=>{if(x)platformsSeen[x]=(platformsSeen[x]||0)+1;}));\nconst platLine=Object.keys(platformsSeen).map(k=>k+' ('+platformsSeen[k]+')').join(', ')||'(none)';\n// --- v0.3 competitor WEBSITE block (Stage 2 reintegration, DEC-129): latest snapshot per domain.\nlet website_snapshots_excluded=0;\nfunction __snapDegraded(s){var q=low(s.quality_status);if(q==='degraded'||q==='quarantined')return true;if(s.report_eligible===false||low(s.report_eligible)==='false')return true;if(/raw_markdown|raw_md|degraded/.test(low(str(s.fallback_reason)+' '+str(s.quality_flags))))return true;return false;}\nconst byDomain={};\nfor(const s of siteSnaps){const d=low(s.domain)||low(str(s.source_url).replace(/^https?:\\/\\/(www\\.)?/,'').split('/')[0]);if(!d)continue;\n if(__snapDegraded(s)){website_snapshots_excluded++;continue;}\n if(!byDomain[d]||str(s.created_at)>str(byDomain[d].created_at))byDomain[d]=s;}\nconst siteLines=Object.keys(byDomain).slice(0,5).map(d=>{const s=byDomain[d];\n return d+' \u2014 '+(str(s.company_name)||'(company n/a)')+': '+cut(str(s.offer_summary)||'(offer n/a)',maxOffer)\n +(str(s.prices_terms)?(' \u00b7 \u0446\u0435\u043d\u044b: '+cut(s.prices_terms,60)):'')\n +(str(s.guarantees)?(' \u00b7 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438: '+cut(s.guarantees,60)):'')\n +(str(s.cta_text)?(' \u00b7 CTA: '+cut(s.cta_text,50)):'')\n +(str(s.change_type)&&low(s.change_type)!=='baseline'?(' \u00b7 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435: '+str(s.change_type)):'');});\nconst webProfiles=profNow.filter(p=>/web|site|\u0441\u0430\u0439\u0442/.test(low(str(p.platforms)+' '+str(p.ad_channels))));\nconst siteChanged=Object.keys(byDomain).filter(d=>['changed','new_offer','price_change'].indexOf(low(byDomain[d].change_type))>=0).length;\n// --- public lead / audience signal block (Lead Scout v0.3, Stage 3.5): AGGREGATES + anonymized summaries.\n// NO contacts/phones/usernames/profile URLs are ever surfaced \u2014 only counts + safe evidence excerpts.\n// Tolerates both v0.3 (review_status/source_platform/score_band/...) and the old 28-col schema (status/platform/...).\nfunction lStatus(l){return low(l.review_status||l.status);}\nfunction lPlatform(l){return low(l.source_platform||l.platform)||'unknown';}\nfunction lPriority(l){return low(l.review_priority);}\nfunction lBand(l){return low(l.score_band);}\nfunction lExcerpt(l){return cut(redact(str(l.evidence_excerpt||l.text_evidence||l.evidence_text)),140);}\nfunction lHasContact(l){return !!(str(l.public_phone)||str(l.public_username)||str(l.public_profile_url)||str(l.contact_public));}\nconst activeLeads=leadSignals.filter(l=>['dismissed','rejected','duplicate','stale'].indexOf(lStatus(l))<0);\nconst newLeads=activeLeads.filter(l=>lStatus(l)==='new'||lStatus(l)==='needs_review').length;\n// priority counts (fall back to band -> priority when review_priority absent)\nfunction prio(l){const p=lPriority(l);if(p)return p;const b=lBand(l);return b==='high'?'high':(b==='medium'?'medium':'low');}\nconst hiLeads=activeLeads.filter(l=>prio(l)==='high').length;\nconst medLeads=activeLeads.filter(l=>prio(l)==='medium').length;\nconst loLeads=activeLeads.filter(l=>prio(l)==='low').length;\n// --- Stage C Closure Patch 2: unambiguous contact counters (C1-D2). Contacts are NEVER rendered.\nconst contacts_detected=activeLeads.filter(l=>lHasContact(l)).length;\nconst contacts_excluded_from_report=contacts_detected;\nconst contacts_redacted=activeLeads.filter(l=>low(l.contact_use_policy)==='do_not_use'||/contact_blanked/.test(low(l.privacy_flags))).length;\nconst report_contains_contacts=false;\nconst contactsPresent=contacts_detected;\nconst contactsBlanked=contacts_redacted;\nconst intentCounts={};const painCounts={};\nfor(const l of activeLeads){const i=low(l.intent_type)||'unknown';intentCounts[i]=(intentCounts[i]||0)+1;\n str(l.pain_type).split(/\\s*,\\s*/).forEach(pn=>{if(pn&&pn!=='unknown')painCounts[pn]=(painCounts[pn]||0)+1;});}\nconst intentLine=Object.keys(intentCounts).map(k=>k+'='+intentCounts[k]).join(', ')||'(none)';\nconst topLeadPains=Object.keys(painCounts).sort((a,b)=>painCounts[b]-painCounts[a]).slice(0,4).map(k=>k+' ('+painCounts[k]+')').join(', ')||'(none)';\nconst leadLine=activeLeads.length?('public_lead_signals: '+activeLeads.length+' (new: '+newLeads+') \u00b7 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 H/M/L: '+hiLeads+'/'+medLeads+'/'+loLeads+' \u00b7 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u044b: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e '+contacts_detected+' / \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0438\u0437 \u043e\u0442\u0447\u0451\u0442\u0430 '+contacts_excluded_from_report+' / \u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043e '+contacts_redacted+' (report_contains_contacts=false) \u00b7 intents: '+intentLine+' \u00b7 top pains: '+topLeadPains+' \u00b7 \u043f\u043e\u043b\u0438\u0442\u0438\u043a\u0430: manual_review/aggregate_only, \u0411\u0415\u0417 \u0430\u0443\u0442\u0440\u0438\u0447\u0430'):(leadSignals.length?('public_lead_signals: '+leadSignals.length+' \u0441\u0442\u0440\u043e\u043a, \u043d\u043e \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u043d\u0435\u0442 \u2014 \u043d\u0435\u0447\u0435\u0433\u043e \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u044c'):'(\u0432\u043a\u043b\u0430\u0434\u043a\u0430 public_lead_signals \u043f\u0443\u0441\u0442\u0430 \u2014 \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF14 Lead Scout triage)');\n// top 3-5 anonymized SAFE summaries (no contacts, no names, no profile URLs) \u2014 manager opens the row to verify.\nconst topLeads=activeLeads.slice().sort((a,b)=>(num(b.lead_score)-num(a.lead_score))).slice(0,5)\n .map((l,i)=>(i+1)+'. ['+(lBand(l)||'-')+' '+num(l.lead_score)+'] '+lPlatform(l)+' \u00b7 '+(str(l.intent_type)||'-')+' \u00b7 \u0431\u043e\u043b\u044c: '+(str(l.pain_type)||'-')+' \u00b7 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435: '+(str(l.recommended_action)||'manual_review')+(lHasContact(l)?' \u00b7 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0430\u043a\u0442-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u043e \u0435\u0441\u0442\u044c (\u0441\u043c. \u0441\u0442\u0440\u043e\u043a\u0443, manual_review)':'')+'\\n \u00ab'+lExcerpt(l)+'\u00bb');\n// --- v0.3 stakeholder executive digest: 5-7 clean bullets, no jargon, no raw ids.\nconst digest=[];\nif(isNoData){digest.push('\u0414\u0430\u043d\u043d\u044b\u0445 \u0437\u0430 \u043e\u043a\u043d\u043e \u043e\u0442\u0447\u0451\u0442\u0430 \u043d\u0435\u0442 \u2014 \u043d\u0443\u0436\u043d\u043e \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440\u044b \u0438\u043b\u0438 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438.');}\nelse{\n digest.push('\u041a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0432 \u043f\u043e\u043b\u0435 \u0437\u0440\u0435\u043d\u0438\u044f: '+profNow.length+'; \u0441\u0430\u043c\u044b\u0439 \u0437\u0430\u043c\u0435\u0442\u043d\u044b\u0439 \u2014 '+(topCompRows.length?cleanName(topCompRows[0]):'(\u043d\u0435\u0442)')+'.');\n if(topAng.length)digest.push('\u0413\u043b\u0430\u0432\u043d\u044b\u0439 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0439 \u0443\u0433\u043e\u043b \u0440\u044b\u043d\u043a\u0430: '+topAng[0]+'.');\n const vk=sigNow.filter(s=>low(s.platform)==='vk')[0];\n const anySig=vk||sigNow[0];\n if(anySig)digest.push('\u0410\u0443\u0434\u0438\u0442\u043e\u0440\u0438\u044f ('+str(anySig.platform)+'): \u0432\u043e\u043f\u0440\u043e\u0441\u043e\u0432 '+num(anySig.question_count)+', \u0432\u043e\u0437\u0440\u0430\u0436\u0435\u043d\u0438\u0439 '+num(anySig.objection_count)+', \u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432 '+num(anySig.buying_intent_count)+(str(anySig.top_pains)?('; \u0431\u043e\u043b\u0438: '+cut(anySig.top_pains,90)):'')+'.');\n if(activeLeads.length)digest.push('\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043b\u0438\u0434-\u0441\u0438\u0433\u043d\u0430\u043b\u043e\u0432: '+activeLeads.length+' ('+newLeads+' \u043d\u043e\u0432\u044b\u0445; \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442 H/M/L '+hiLeads+'/'+medLeads+'/'+loLeads+') \u2014 \u043d\u0430 \u0440\u0443\u0447\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u043c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u043e\u043c, \u0431\u0435\u0437 \u043a\u0430\u043a\u043e\u0433\u043e-\u043b\u0438\u0431\u043e \u0430\u0443\u0442\u0440\u0438\u0447\u0430'+(contactsPresent?('; \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0445 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u043e\u0432-\u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043b\u044c\u0441\u0442\u0432: '+contactsPresent):'')+'.');\n if(siteLines.length)digest.push('\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: '+siteLines.length+' \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435'+(siteChanged?(', \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439: '+siteChanged):'')+'.');\n digest.push('\u0414\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u043d\u043e\u0441\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432: '+(confScores.length?('\u0432 \u0441\u0440\u0435\u0434\u043d\u0435\u043c '+confAvg+'/100'):'\u043d\u0435\u0442 \u043e\u0446\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439')+'; \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0441\u043c\u0435\u0448\u0430\u043d\u043d\u044b\u0435 (live + \u0438\u0441\u0442\u043e\u0440\u0438\u0447\u0435\u0441\u043a\u0438\u0435 + \u0432\u0435\u0431).');\n if(topAng.length)digest.push('\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u044b\u0439 \u0444\u043e\u043a\u0443\u0441 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430: '+cut(str((angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency))[0]||{}).recommended_content_response),110)+'.');\n}\nconst digestMd=digest.slice(0,7).map(b=>'- '+b).join('\\n');\n// --- v0.3 action blocks (deterministic, no outreach)\nconst mgrActions=isNoData?['\u0420\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u0444\u0438\u043b\u044c\u0442\u0440\u044b/\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438 \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF10 + WF12.']:[\n (activeLeads.length?('\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c '+newLeads+' \u043d\u043e\u0432\u044b\u0445 \u0441\u0442\u0440\u043e\u043a public_lead_signals (review_status=new, \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442=high): set accepted/rejected/duplicate/stale. \u041a\u043e\u043d\u0442\u0430\u043a\u0442\u044b \u2014 \u0442\u043e\u043b\u044c\u043a\u043e manual_review, \u0411\u0415\u0417 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439; outreach_allowed=false.'):'\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c WF14 (Lead Scout triage), \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c public_lead_signals.'),\n '\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0442\u043e\u043f-\u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438 \u0443\u0433\u043b\u044b (competitor_profiles / market_angles): \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c, \u043a\u043e\u0433\u043e \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435.',\n (siteLines.length?(siteChanged>0?('\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0435\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0430 \u0441\u0430\u0439\u0442\u0430\u0445 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 ('+siteChanged+' \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432).'):'\u0421\u0430\u0439\u0442\u044b \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432: \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u0437\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 \u043d\u0435\u0442 \u2014 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043f\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044e \u043d\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f.'):'\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c competitor_site_snapshots: \u043f\u0440\u043e\u0433\u043d\u0430\u0442\u044c WF04 \u043f\u043e \u0441\u0430\u0439\u0442\u0430\u043c \u0442\u043e\u043f-\u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (\u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).')];\nconst contentActions=isNoData?[]:angNow.slice().sort((a,b)=>num(b.frequency)-num(a.frequency)).slice(0,3).map(a=>cut(str(a.recommended_content_response),130)).filter(Boolean);\nconst sourceActions=isNoData?['\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u043d\u043d\u0435\u043a\u0442\u043e\u0440\u044b (WF09/WF11/WF13) \u0438 WF08, \u0437\u0430\u0442\u0435\u043c WF10.']:[\n 'Avito (WF09): live \u0438 \u0438\u0441\u0442\u043e\u0440\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u2014 \u043f\u043b\u0430\u043d\u043e\u0432\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u043f\u043e \u043b\u0438\u043c\u0438\u0442\u0430\u043c \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 (COSTS_AND_LIMITS).',\n 'Telegram (WF11): \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0439 \u043f\u0440\u0435\u0432\u044c\u044e \u043f\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u043c \u043a\u0430\u043d\u0430\u043b\u0430\u043c \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d \u2014 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0447\u0435\u0440\u0435\u0437 approval gate, \u043b\u0438\u043c\u0438\u0442\u044b \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u0438 \u0444\u0438\u043b\u044c\u0442\u0440 \u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u043e\u0441\u0442\u0438. \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u2014 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c/\u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 \u0438 \u043e\u0434\u043e\u0431\u0440\u0438\u0442\u044c \u0437\u0430\u043f\u0443\u0441\u043a (\u0431\u0435\u0437 \u0430\u0443\u0442\u0440\u0438\u0447\u0430).',\n 'VK (WF13): \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435 VK \u043d\u0435 \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u043e\u0442\u0447\u0451\u0442\u0430 C.1, \u043d\u043e live-smoke C.4 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u0435\u043d \u0434\u043e \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f Stage C/MVP. \u0422\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f \u0438 VK-\u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\u0430 (\u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API/HTTP-\u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442).',\n (siteLines.length?'\u0421\u0430\u0439\u0442\u044b: \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u044b \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u043c \u043f\u0440\u043e\u0433\u043e\u043d\u043e\u043c \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 Stage 2 (WF04/WF06, \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).':'\u0421\u0430\u0439\u0442\u044b: competitor_site_snapshots \u0435\u0449\u0451 \u043d\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d \u2014 \u043f\u043e\u0441\u043b\u0435 Stage 2 cleanup \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u0432\u0435\u0431-\u0441\u043d\u0430\u043f\u0448\u043e\u0442\u043e\u0432 \u043f\u043e 3\u20135 \u0442\u043e\u043f-\u0441\u0430\u0439\u0442\u0430\u043c \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (WF04/WF06, \u043f\u043e \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044e).')];\nfunction ol(arr){return arr.map((x,i)=>(i+1)+'. '+x).join('\\n')||'(\u043d\u0435\u0442)';}\n// --- mandatory source-mix label (DEC-108)\nconst SOURCE_MIX='mixed: live + historical/manual + web pipeline';\nconst reportType=isNoData?'no_data_notice':str(cfg.report_type);\n// --- deterministic Markdown render (v0.3 stakeholder report)\nconst md=[\n'# Market intelligence report '+str(cfg.report_id)+' (v0.3)',\n(isFixtureReport?'> **TEST / FIXTURE DATA \u2014 NOT PRODUCTION INTELLIGENCE**':''),\n(quality_warning?('> **QUALITY WARNING:** '+quality_warning):''),\n'',\n'- niche: '+str(cfg.niche_id)+' \u00b7 region: '+str(cfg.region)+' \u00b7 WF10 run: wf10_'+stamp+(prevStamp?(' \u00b7 prev: wf10_'+prevStamp):' \u00b7 prev: (\u043d\u0435\u0442 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0431\u0430\u0437\u043e\u0432\u043e\u0433\u043e \u043f\u0440\u043e\u0433\u043e\u043d\u0430 \u2014 trend_status=no_compatible_baseline)'),\n'- **\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438: '+SOURCE_MIX+'** \u00b7 rows_after_filters='+rowsAfter,\n'',\n'## \u0414\u0430\u0439\u0434\u0436\u0435\u0441\u0442 \u0434\u043b\u044f \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f (executive_digest)',digestMd,\n'',\nisNoData?('**NO DATA:** '+NO_DATA_TEXT):[\n'## \u0421\u043d\u0430\u043f\u0448\u043e\u0442 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 (competitor_snapshot)','\u043f\u0440\u043e\u0444\u0438\u043b\u0435\u0439 \u0432 \u0441\u043d\u0430\u043f\u0448\u043e\u0442\u0435: '+profNow.length+' \u00b7 \u043f\u043b\u043e\u0449\u0430\u
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
12 - Market Intelligence Report Builder (Stakeholder v0.3). Uses googleSheets, httpRequest. Event-driven trigger; 23 nodes.
Source: https://github.com/CodeVinci8/marketing-scout-agent/blob/4ba4e522589d38a961dc130a16b32ae62e8a4119/n8n/workflows/12_market_intelligence_report_builder.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.
Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 59 nodes.
CLEAN Agent - Manual Trigger. Uses googleDrive, googleSheets, httpRequest. Event-driven trigger; 49 nodes.
Secretary — Gmail Manager (gelsonmascarenhas@gmail.com). Uses gmailTrigger, httpRequest, googleCalendar, gmail. Event-driven trigger; 47 nodes.
💥 AI Image to Professional Video Workflow using NanoBanana Ultra & Kling AI. Uses googleSheets, googleDrive, httpRequest, editImage. Event-driven trigger; 45 nodes.
Template 13195. Uses slackTrigger, googleSheets, httpRequest, slack. Event-driven trigger; 31 nodes.