AutomationFlowsAI & RAG › Market Intelligence Report Builder (stakeholder V0.3)

Market Intelligence Report Builder (stakeholder V0.3)

12 - Market Intelligence Report Builder (Stakeholder v0.3). Uses googleSheets, httpRequest. Event-driven trigger; 23 nodes.

Event trigger★★★★☆ complexity23 nodesGoogle SheetsHTTP Request
AI & RAG Trigger: Event Nodes: 23 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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.

Pro

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 →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Inquiry-Agent. Uses @digitalocean/n8n-nodes-digitalocean-gradient-serverless-inference, stopAndError, googleDocs, gmail. Event-driven trigger; 59 nodes.

@Digitalocean/N8N Nodes Digitalocean Gradient Serverless Inference, Stop And Error, Google Docs +4
AI & RAG

CLEAN Agent - Manual Trigger. Uses googleDrive, googleSheets, httpRequest. Event-driven trigger; 49 nodes.

Google Drive, Google Sheets, HTTP Request
AI & RAG

Secretary — Gmail Manager (gelsonmascarenhas@gmail.com). Uses gmailTrigger, httpRequest, googleCalendar, gmail. Event-driven trigger; 47 nodes.

Gmail Trigger, HTTP Request, Google Calendar +2
AI & RAG

💥 AI Image to Professional Video Workflow using NanoBanana Ultra & Kling AI. Uses googleSheets, googleDrive, httpRequest, editImage. Event-driven trigger; 45 nodes.

Google Sheets, Google Drive, HTTP Request +2
AI & RAG

Template 13195. Uses slackTrigger, googleSheets, httpRequest, slack. Event-driven trigger; 31 nodes.

Slack Trigger, Google Sheets, HTTP Request +2