This workflow follows the Execute Workflow Trigger → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "08 - Touchpoint Analyzer",
"nodes": [
{
"id": "a8000000-0000-0000-0000-0000000000n1",
"name": "Overview Note RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-540,
-320
],
"parameters": {
"content": "## 08 \u2014 Touchpoint Analyzer (Stage 3.2, PATCHED v3 \u2014 DETERMINISTIC-FIRST) \u2014 Business Scout Agent\n\nSource-agnostic \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440. \u0427\u0438\u0442\u0430\u0435\u0442 approved/unique \u0438\u0437 raw_market_records, \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u0442 \u0432 6 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043e\u043a (35 \u043a\u043e\u043b\u043e\u043d\u043e\u043a).\n\n\u041f\u0410\u0422\u0427 v3 (DEC-082): \u0432\u0442\u043e\u0440\u043e\u0439 live-\u0442\u0435\u0441\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043b primary_json=0, \u0434\u043e\u0440\u043e\u0433\u043e\u0439 \u0438 \u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439 LLM ($0.159 \u0437\u0430 12 \u0437\u0430\u043f\u0438\u0441\u0435\u0439). \u0422\u0435\u043f\u0435\u0440\u044c \u0430\u043d\u0430\u043b\u0438\u0437\u0430\u0442\u043e\u0440 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u041e-\u041f\u0415\u0420\u0412\u042b\u0419. Claude \u2014 \u041e\u041f\u0426\u0418\u041e\u041d\u0410\u041b\u042c\u041d\u041e\u0415 \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435, \u0412\u042b\u041a\u041b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.\n\nSet Analyzer Config: analysis_mode='deterministic_first', llm_enrichment=false, max_records=12, test_mode=true.\n\n\u041b\u043e\u0433\u0438\u043a\u0430:\n- Prepare Record \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u0414\u0415\u0422\u0415\u0420\u041c\u0418\u041d\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u0423\u042e \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e (det) \u0434\u043b\u044f \u041a\u0410\u0416\u0414\u041e\u0419 \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430\u043c \u043f\u0440\u0438\u0451\u043c\u0430.\n- LLM-\u0448\u043b\u044e\u0437 (IF Call Claude?): call_claude = (\u041d\u0415 irrelevant) \u0418 (llm_enrichment=true \u0418\u041b\u0418 deterministic_needs_llm=true).\n- \u041f\u0440\u0438 deterministic_first + llm_enrichment=false \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0434\u0443\u0442 \u0432 Build Deterministic Row \u0411\u0415\u0417 Claude ($0): irrelevant \u2192 deterministic_irrelevant_skip; \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u2192 deterministic_pre_route.\n- Claude \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 det \u043d\u0435\u0443\u0432\u0435\u0440\u0435\u043d (deterministic_needs_llm=true) \u0438\u043b\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435. \u041f\u043e\u0441\u043b\u0435 Claude: primary \u2192 repair \u2192 \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 fallback (deterministic_fallback_after_llm_fail). technical_errors \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 det \u0431\u0435\u0437 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0433\u043e route \u0438\u043b\u0438 \u0441\u0431\u043e\u0439 Sheets/API.\n\n\u0414\u043b\u044f 12-\u0437\u0430\u043f\u0438\u0441\u043d\u043e\u0433\u043e \u0444\u0438\u043a\u0441\u0442\u0443\u0440\u0430 (deterministic_first, enrichment off): Claude calls=0, repair_used=0, technical_errors=0.\n\n\u0411\u0443\u0434\u0443\u0449\u0438\u0439 \u0440\u0435\u0436\u0438\u043c: analysis_mode='llm_enriched' + llm_enrichment=true \u2192 Claude \u043e\u0431\u043e\u0433\u0430\u0449\u0430\u0435\u0442 \u043d\u0435-irrelevant \u0437\u0430\u043f\u0438\u0441\u0438, fallback \u043f\u043e-\u043f\u0440\u0435\u0436\u043d\u0435\u043c\u0443 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442.\n\nparse_method: deterministic_pre_route | deterministic_irrelevant_skip | primary_json | repaired_json | deterministic_fallback_after_llm_fail | technical_error.\n\n\u041f\u0410\u0422\u0427 v6 (DEC-087, \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f \u043f\u043e\u0441\u043b\u0435 C2 PARTIAL PASS): (A) \u043a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u043c\u043f\u0442 \u0434\u043b\u044f source_candidate/review_source (Telegram/Zoon) + \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 HINTS (expected_entity_type/action/route, no_contact_safety, forbidden_phrases) \u0432 payload. (B) review_source: \u0442\u043e\u043b\u044c\u043a\u043e \u041d\u0410\u0417\u0412\u0410\u041d\u041d\u042b\u0419 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442 (competitor_name) \u2192 monitor_queue; \u043e\u0431\u043e\u0431\u0449\u0451\u043d\u043d\u044b\u0439 \u043a\u0430\u0442\u0430\u043b\u043e\u0433/\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u043e\u0442\u0437\u044b\u0432\u043e\u0432 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, Zoon \u00ab\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u044b \u0432 \u041c\u043e\u0441\u043a\u0432\u0435\u00bb) \u2192 content_idea/content_queue, competitor_strength \u226445. (C) \u0431\u0435\u0437 \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0430 reason \u041d\u0415 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u00ab\u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e/\u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c/\u043f\u043e\u0437\u0432\u043e\u043d\u0438\u0442\u044c/\u0441\u0432\u044f\u0437\u0430\u0442\u044c\u0441\u044f\u00bb \u2014 \u0441\u0430\u043d\u0438\u0442\u0430\u0439\u0437\u0435\u0440 \u0432 Merge \u0437\u0430\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430 \u00ab\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0440\u0443\u0447\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438\u00bb. (D) \u0437\u0430\u043f\u0440\u0435\u0442 \u043d\u0435\u043e\u0431\u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u00ab\u0441\u043f\u0440\u043e\u0441 \u0440\u0430\u0441\u0442\u0451\u0442/\u043c\u043d\u043e\u0433\u043e \u043b\u0438\u0434\u043e\u0432/\u0432\u044b\u0441\u043e\u043a\u0430\u044f \u043a\u043e\u043d\u0432\u0435\u0440\u0441\u0438\u044f\u00bb. max_tokens \u041d\u0415 \u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d. \u0426\u0435\u043b\u044c C3: primary_json\u22653/4, fallback\u22641/4, technical_errors=0.\n\n\u041f\u0410\u0422\u0427 v7 (DEC-088, C4 \u2014 \u0441\u0442\u0430\u0431\u0438\u043b\u0438\u0437\u0430\u0446\u0438\u044f JSON \u0434\u043b\u044f Telegram/source_candidate): \u0434\u043b\u044f source_candidate / social_channel / telegram-content_idea \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0421\u041f\u0415\u0426\u0418\u0410\u041b\u0418\u0417\u0418\u0420\u041e\u0412\u0410\u041d\u041d\u0410\u042f \u0423\u041b\u042c\u0422\u0420\u0410-\u041a\u041e\u0420\u041e\u0422\u041a\u0410\u042f \u0441\u0445\u0435\u043c\u0430 \u0438\u0437 7 \u043a\u043b\u044e\u0447\u0435\u0439 (profile_name, service_type, offer_text, detected_need, reason, content_idea_score, quality_score) \u2014 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u043f\u0440\u043e\u043c\u043f\u0442 \u0438 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0439 payload \u0432 Build Primary Claude Request; \u043c\u0435\u043d\u044c\u0448\u0430\u044f \u043f\u043e\u0432\u0435\u0440\u0445\u043d\u043e\u0441\u0442\u044c \u043e\u0442\u0432\u0435\u0442\u0430 \u043f\u043e\u0432\u044b\u0448\u0430\u0435\u0442 \u043d\u0430\u0434\u0451\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u0442\u0440\u043e\u0433\u043e\u0433\u043e JSON \u0438 \u0443\u0431\u0438\u0440\u0430\u0435\u0442 fallback Telegram. Repair \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0435\u043c\u0435\u0439\u0441\u0442\u0432\u0430 \u0447\u0438\u043d\u0438\u0442 \u0432 \u0442\u0435 \u0436\u0435 7 \u043a\u043b\u044e\u0447\u0435\u0439 (\u043d\u0435 \u0440\u0430\u0437\u0434\u0443\u0432\u0430\u0435\u0442 \u0434\u043e 15). Merge \u043d\u0430\u043a\u043b\u0430\u0434\u044b\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u044d\u0442\u0438 7 \u043f\u043e\u043b\u0435\u0439, \u0430 route=content_queue / entity_type=content_idea / recommended_action=create_content / lead_signal_score=1 / competitor_strength=1 / contact_public='' \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0434\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438. \u041f\u043e\u0441\u0442-merge safety assertion (Task D) \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0440\u0443\u0435\u0442 \u044d\u0442\u043e \u0434\u043b\u044f social-\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. max_tokens \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u0443\u0442\u0438 500/400 (\u041d\u0415 \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c). \u0426\u0435\u043b\u044c C4: primary_json\u22653/4, Telegram \u043d\u0435 fallback, technical_errors=0.\n\n\u041d\u0415 \u043f\u0430\u0440\u0441\u0435\u0440, \u041d\u0415 \u0441\u043a\u0440\u0435\u0439\u043f\u0438\u043d\u0433, \u041d\u0415 Apify/Firecrawl. \u0422\u043e\u043b\u044c\u043a\u043e Claude HTTP (gated). active=false.",
"height": 700,
"width": 660,
"color": 4
}
},
{
"id": "a8000000-0000-0000-0000-0000000000n2",
"name": "Test Instructions RU",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
160,
-320
],
"parameters": {
"content": "## \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438 \u0442\u0435\u0441\u0442 (\u0432\u0440\u0443\u0447\u043d\u0443\u044e)\n\n1. \u041d\u0415 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c (active=false).\n2. \u0412\u043a\u043b\u0430\u0434\u043a\u0438: 6 \u0431\u0438\u0437\u043d\u0435\u0441-\u0432\u043a\u043b\u0430\u0434\u043e\u043a (35 \u043a\u043e\u043b\u043e\u043d\u043e\u043a) + raw_market_records (40). \u0423\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442.\n3. \u041f\u041e\u0421\u041b\u0415 \u0418\u041c\u041f\u041e\u0420\u0422\u0410 \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\u044b (ID \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b):\n - Claude Primary / Claude Repair \u2192 Claude API - Marketing Scout\n - Read raw_market_records / Append to Dynamic Route Sheet \u2192 Google Sheets - Marketing Scout Service Account\n4. \u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE \u043d\u0430 \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 Spreadsheet ID (2 \u043d\u043e\u0434\u044b).\n\n\u0420\u0415\u0422\u0415\u0421\u0422 DETERMINISTIC-FIRST (DEC-082, 12 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 Workflow 07):\n- analysis_mode='deterministic_first', llm_enrichment=false (\u0434\u0435\u0444\u043e\u043b\u0442).\n- \u0411\u0430\u043b\u0430\u043d\u0441 Claude \u0414\u041e \u2192 Execute Workflow \u2192 \u0431\u0430\u043b\u0430\u043d\u0441 \u041f\u041e\u0421\u041b\u0415 \u2192 docs/STAGE_3_2_TEST_RESULTS.md.\n\n\u041e\u0416\u0418\u0414\u0410\u0415\u041c\u041e (deterministic_first, \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u0435 \u0412\u042b\u041a\u041b):\n- Claude Primary API Request \u041d\u0415 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f. Claude Repair API Request \u041d\u0415 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.\n- Claude cost delta = $0. repair_used=false \u0434\u043b\u044f \u0432\u0441\u0435\u0445 12.\n- parse_method: deterministic_pre_route = 10, deterministic_irrelevant_skip = 2.\n- technical_errors = 0.\n\n\u041e\u0436\u0438\u0434\u0430\u0435\u043c\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f:\n- 1,2 Avito competitor \u2192 monitor_queue (deterministic_pre_route).\n- 3,4 Dzen competitor channel/post \u2192 monitor_queue.\n- 5 VK source_candidate \u2192 content_queue.\n- 7,8 Telegram source_candidate \u2192 content_queue.\n- 6 Yandex Maps reviews (competitor_related) \u2192 monitor_queue.\n- 12 Zoon reviews (competitor_related) \u2192 monitor_queue.\n- 9,10 irrelevant \u2192 skipped_log (deterministic_irrelevant_skip, $0).\n- 11 Banki \u0444\u043e\u0440\u0443\u043c (hot pattern, \u0431\u0435\u0437 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0430) \u2192 review_queue, entity_type=lead_signal, recommended_action=investigate, lead_signal_score=75, needs_manual_review=true (deterministic_pre_route).\n\n\u0411\u0443\u0434\u0443\u0449\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0430\u0449\u0435\u043d\u0438\u044f: \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c analysis_mode='llm_enriched' + llm_enrichment=true \u2192 Claude \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043d\u0435-irrelevant \u0437\u0430\u043f\u0438\u0441\u044f\u043c, fallback \u043f\u043e det \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442\u0441\u044f.",
"height": 700,
"width": 640,
"color": 4
}
},
{
"id": "a8000000-0000-0000-0000-000000000002",
"name": "Manual Start",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
120,
320
],
"parameters": {}
},
{
"id": "a8000000-0000-0000-0000-0000000000cf",
"name": "Set Analyzer Config",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
320,
320
],
"parameters": {
"jsCode": "// Analyzer config. deterministic_first by default, LLM enrichment OFF. No external calls by default.\n// MSK time (DEC-083): operational timestamps + run_id use explicit Moscow time +03:00.\nfunction pad(n){return String(n).padStart(2,'0');}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){const m=new Date(Date.now()+10800000);return m.getUTCFullYear()+pad(m.getUTCMonth()+1)+pad(m.getUTCDate())+'_'+pad(m.getUTCHours())+pad(m.getUTCMinutes())+pad(m.getUTCSeconds());}\nconst now=moscowIsoNow();\nconst stamp=moscowStamp();\nvar __cfg = {\n llm_enabled:false, // MASTER kill switch (DEC-119). false => NO Claude call (safe offline default).\n analysis_mode:'llm_primary', // \u00a72.1 PRODUCTION target mode. Claude is the PRIMARY semantic classifier for\n // substantive live records (may override WF09/WF11 hints from POST_EVIDENCE).\n // Deterministic logic = Stage A pre-gate + dedup + system-event + hard-negative +\n // safety + freshness + fallback + free fixture/offline classifier (semantic_core).\n // Runs only when llm_enabled=true (operator approval). Until then the deterministic\n // offline classifier (evidence-based, taxonomy-correct) produces semantic-v2 rows.\n data_mode:'fixture', // \u00a713 fixture | live | manual_test \u2014 set to the mode you actually run.\n taxonomy_version:'semantic-v2.0',\n semantic_schema_version:'semantic-v2',\n freshness_window_days:30,\n llm_enrichment:false,\n llm_enrichment_test_mode:false,\n llm_test_batch_indexes:[1,7,11,12],\n test_mode:true,\n max_records:12,\n analyze_statuses:['approved','new'],\n production_statuses:['approved'],\n agent_request_id_filter:'',\n source_run_id_filter:'', // \u00a72.4 exact source-run isolation (canonical join key)\n force_reprocess:false, // \u00a72.4 exactly-once: re-analyze an already-analyzed source_record only when true\n require_quality_eligible:true, // \u00a72.6 record-level gate: degraded/quarantined/pending never reach analysis\n analysis_run_id:'analysis_'+stamp, // one analyzer execution id (lineage)\n require_llm_approval:true, // \u00a71 runtime guard: an explicit token is required to call Claude\n llm_approval_token:'', // operator pastes the approved token here for an LLM run (value never logged)\n expected_llm_approval_token:'WF08_LLM_APPROVED',\n llm_budget_usd:0.50, // \u00a71 hard budget; a per-call estimate above this blocks the call\n estimated_llm_cost_per_call_usd:0.01,\n request_cancelled:false, // \u00a71 a cancelled request never calls Claude\n platform_filter:'',\n source_type_filter:'',\n run_id:'touchpoint_'+stamp,\n generated_at:now,\n note:'COST-CONTROL MODE (DEC-119): llm_enabled=false is the MASTER kill switch \u2014 when false, NO record reaches Claude nodes regardless of llm_enrichment / llm_enrichment_test_mode / deterministic_needs_llm; uncertain records route to review_queue with parse_method=deterministic_uncertain_no_llm, estimated_analysis_cost_usd=0. Set llm_enabled=true (explicit operator approval) to re-enable the existing LLM path. deterministic_first + llm_enrichment=false: obvious records route WITHOUT Claude (deterministic_pre_route / deterministic_irrelevant_skip). Only deterministic_needs_llm=true records call Claude. Future mode: analysis_mode=llm_enriched + llm_enrichment=true enriches non-irrelevant records (deterministic fallback still applies). LLM 4-record test: set llm_enrichment_test_mode=true (sends only llm_test_batch_indexes through Claude). Source handoff (Stage 3.3): set agent_request_id_filter/platform_filter/source_type_filter (empty by default) to process only rows from one connector run; these apply BEFORE max_records and BEFORE per-record processing, and are independent of llm_test_batch_indexes (do NOT use batch indexes for source handoff). production: set test_mode=false -> only approved analyzed and irrelevant excluded.'\n};\n\nvar __agentIn=(typeof $json==='object'&&$json)?$json:{};\nfunction __ov(ck,ik){var k=ik||ck;var v=__agentIn[k];if(v!==undefined&&v!==null&&v!=='')__cfg[ck]=v;}\n__ov('agent_request_id_filter','agent_request_id');\n__ov('source_run_id_filter','source_run_id');\n__ov('data_mode','data_mode');\n__ov('force_reprocess','force_reprocess');\n__ov('platform_filter','platform_filter');\n__ov('source_type_filter','source_type_filter');\n__ov('llm_enabled','llm_enabled');\n__ov('llm_approval_token','llm_approval_token');\n__ov('llm_budget_usd','llm_budget_usd');\n__ov('request_cancelled','request_cancelled');\n__ov('max_records','max_records');\n// WF08-LLMAGENT-001: executeWorkflow inputs arrive as STRINGS; coerce boolean/number overrides so the\n// strict guards (cfg.llm_enabled===true) see real booleans. 'false'/'' stay false \u2014 manual defaults unchanged.\nfunction __boolish(v){return v===true||String(v).toLowerCase()==='true';}\n__cfg.llm_enabled=__boolish(__cfg.llm_enabled);\n__cfg.force_reprocess=__boolish(__cfg.force_reprocess);\n__cfg.request_cancelled=__boolish(__cfg.request_cancelled);\n__cfg.llm_budget_usd=Number(__cfg.llm_budget_usd)||0;\nreturn [{ json: __cfg }];"
}
},
{
"id": "a8000000-0000-0000-0000-0000000000rd",
"name": "Read raw_market_records",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
540,
320
],
"alwaysOutputData": true,
"onError": "continueRegularOutput",
"parameters": {
"authentication": "serviceAccount",
"operation": "read",
"documentId": {
"__rl": true,
"value": "={{ $env.MS_SPREADSHEET_ID || \"PASTE_SPREADSHEET_ID\" }}",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "raw_market_records",
"mode": "name"
},
"options": {}
},
"credentials": {
"googleApi": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000
},
{
"id": "a8000000-0000-0000-0000-0000000000fl",
"name": "Filter & Select Records",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
760,
320
],
"parameters": {
"jsCode": "// Select records to analyze. dedup_status=unique + approval_status in allowed. test_mode includes irrelevant.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\n// SCOPE-STRICT-001: accept the whole request run family (see WF16).\nfunction runFamilyMatch(rid,f){rid=String(rid==null?'':rid).trim();f=String(f==null?'':f).trim();if(!f)return true;if(!rid)return false;return rid===f||rid.indexOf(f+'::')===0||f.indexOf(rid+'::')===0;}\nconst cfg=$('Set Analyzer Config').first().json;\nconst test_mode=(cfg.test_mode===true);\nconst allowed=(test_mode?(cfg.analyze_statuses||['approved','new']):(cfg.production_statuses||['approved'])).map(s=>String(s).toLowerCase());\nconst include_irrelevant=test_mode;\nconst max_records=parseInt(cfg.max_records)||12;\nconst run_id=cfg.run_id||'';\n// Source handoff filters (Stage 3.3, DEC-091): empty by default = unchanged behavior. Applied BEFORE max_records\n// and BEFORE per-record processing so only rows from one connector run (e.g. a Workflow 09 Avito request) flow.\nconst agent_request_id_filter=str(cfg.agent_request_id_filter);\nconst source_run_id_filter=str(cfg.source_run_id_filter);\nconst force_reprocess=(cfg.force_reprocess===true);\nconst require_quality=(cfg.require_quality_eligible!==false);\nfunction canonId(x){return str(x.source_run_id)||str(x.run_id)||str(x.agent_request_id);}\n// \u00a72.4 exactly-once: keys of source records already analyzed (from the analysis_runs ledger).\nconst analyzedKeys=(function(){var m={};try{$('Read analysis_runs').all().map(function(i){return i.json;}).forEach(function(g){var k=str(g.analysis_idempotency_key)||(str(g.source_run_id)+'::'+str(g.source_record_id));if(k)m[k]=1;});}catch(e){}return m;})();\nconst platform_filter=low(cfg.platform_filter);\nconst source_type_filter=low(cfg.source_type_filter);\nconst rows=$('Read raw_market_records').all().map(i=>i.json).filter(Boolean);\nconst out=[];\nfor(const r of rows){\n if(!str(r.record_id))continue;\n if(low(r.dedup_status)!=='unique')continue;\n if(!allowed.includes(low(r.approval_status)))continue;\n const rth=low(r.record_type_hint)||'unknown';\n if(rth==='irrelevant'&&!include_irrelevant)continue;\n if(agent_request_id_filter&&str(r.agent_request_id)!==agent_request_id_filter)continue;\n if(source_run_id_filter&&!runFamilyMatch(canonId(r),source_run_id_filter))continue;\n // \u00a72.6 record-level quality gate (mixed run: each record gated on its OWN quality, not the run)\n if(require_quality){ var __qs=low(r.quality_status); if(__qs==='degraded'||__qs==='quarantined')continue; if(low(r.review_status)==='pending')continue; if(low(r.analysis_status)==='analyzed'||low(r.analysis_status)==='done')continue; }\n var __sid=canonId(r), __rid=str(r.source_record_id)||str(r.record_id), __key=__sid+'::'+__rid;\n if(!force_reprocess&&analyzedKeys[__key])continue; // \u00a72.4 exactly-once\n if(platform_filter&&low(r.platform)!==platform_filter)continue;\n if(source_type_filter&&low(r.source_type)!==source_type_filter)continue;\n if(out.length>=max_records)break;\n out.push({ json: {\n record_id:str(r.record_id), agent_request_id:str(r.agent_request_id),\n source_run_id:__sid, source_record_id:__rid, workflow_run_id:str(r.workflow_run_id), data_mode:low(r.data_mode)||'live',\n quality_status:low(r.quality_status), analysis_status:low(r.analysis_status)||'pending',\n analysis_idempotency_key:__key, analysis_run_id:str(cfg.analysis_run_id)||run_id,\n source_type:str(r.source_type),\n platform:str(r.platform), source_url:str(r.source_url), post_url:str(r.post_url), profile_url:str(r.profile_url),\n profile_name:str(r.profile_name), author_handle:str(r.author_handle), published_at:str(r.published_at),\n region_hint:str(r.region_hint), service_hint:str(r.service_hint), text_context:str(r.text_context),\n comment_text:str(r.comment_text), contact_public:str(r.contact_public), record_type_hint:rth,\n touchpoint_type:low(r.touchpoint_type), lead_intent_hint:low(r.lead_intent_hint), urgency_hint:low(r.urgency_hint),\n lead_temperature:low(r.lead_temperature), competitor_related:r.competitor_related, competitor_name:str(r.competitor_name),\n interest_topic:str(r.interest_topic), probable_need:str(r.probable_need),\n run_id:run_id, batch_index:out.length+1\n }});\n}\n// C2 fix (DEC-086): when llm_enrichment_test_mode=true, pre-filter the SELECTED records to llm_test_batch_indexes\n// BEFORE the processing loop, so the loop receives exactly the test fixtures (e.g. [1,7,11,12] -> 4 items).\n// batch_index was assigned over the FULL selection above, so indexes stay stable (1,7,11,12). This avoids any\n// empty return inside Build Deterministic Row, which would stall Split-in-Batches loop continuation.\nconst llm_test=(cfg.llm_enrichment_test_mode===true);\nconst testIdx=Array.isArray(cfg.llm_test_batch_indexes)?cfg.llm_test_batch_indexes.map(Number):[];\nconst before=out.length;\nlet selected=out;\nif(llm_test&&testIdx.length>0){\n selected=out.filter(it=>testIdx.indexOf(Number(it.json.batch_index))>=0);\n}\nconst after=selected.length;\nfor(const it of selected){\n it.json.selected_count_before_test_filter=before;\n it.json.selected_count_after_test_filter=after;\n it.json.llm_enrichment_test_mode=llm_test;\n it.json.llm_test_batch_indexes=testIdx;\n}\nreturn selected;"
}
},
{
"id": "a8000000-0000-0000-0000-0000000000lp",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
980,
320
],
"parameters": {
"options": {}
}
},
{
"id": "a8000000-0000-0000-0000-0000000000pr",
"name": "Prepare Record",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
440
],
"parameters": {
"jsCode": "// Per-record holder. Builds analyzer text AND a DETERMINISTIC classification (det) from intake hints.\n// deterministic_first (DEC-082): det is the PRIMARY classifier. Claude is OPTIONAL enrichment.\n// det decides (a) is_irrelevant pre-Claude, (b) deterministic_needs_llm gate, (c) the row when Claude is skipped,\n// (d) the fallback if Claude+repair fail.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction low(v){return str(v).toLowerCase();}\nfunction cleanContact(raw){\n const s=String(raw||'').trim();\n if(!s)return '';\n if(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/.test(s))return s;\n if(/t\\.me\\/|wa\\.me\\//i.test(s))return s;\n if(/(^|[^A-Za-z0-9_])@[A-Za-z0-9_]{4,}/.test(s))return s;\n const digits=s.replace(/\\D/g,'');\n if(digits.length>=10&&digits.length<=11)return s;\n return '';\n}\nfunction host(u){try{return new URL(str(u)).hostname.replace(/^www\\./,'');}catch(e){return '';}}\n// Avito/classified competitor ad-intelligence det builder (DEC-092). Parses the WF09 text_context signature\n// 'Avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435: <TITLE> | \u0446\u0435\u043d\u0430: <PRICE> | <LOCATION> | \u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446: <SELLER> | <DESC> | \u0437\u0430\u043f\u0440\u043e\u0441: <QUERY>'\n// to populate offer_text/terms/service_type/scores. Deterministic, no Claude. lead_signal_score=1 (competitor ad).\nfunction classifiedCompetitorDet(r,tc){\n function s2(v){return v==null?'':String(v).trim();}\n let body=s2(tc).replace(/^avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435:\\s*/i,'');\n const parts=body.split(' | ');\n let title='',price='',seller='',q='';\n const leftover=[];\n parts.forEach(function(p,idx){ const t=s2(p);\n if(idx===0){title=t;return;}\n if(/^\u0446\u0435\u043d\u0430:/i.test(t)){price=t.replace(/^\u0446\u0435\u043d\u0430:\\s*/i,'');}\n else if(/^\u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446:/i.test(t)){seller=t.replace(/^\u043f\u0440\u043e\u0434\u0430\u0432\u0435\u0446:\\s*/i,'');}\n else if(/^\u0437\u0430\u043f\u0440\u043e\u0441:/i.test(t)){q=t.replace(/^\u0437\u0430\u043f\u0440\u043e\u0441:\\s*/i,'');}\n else leftover.push(t);\n });\n const loc=leftover.length?leftover[0]:'';\n const description=leftover.slice(1).join(' | ');\n const blob=(title+' '+description+' '+q).toLowerCase();\n const classifiedSvc=['credit_broker','business_credit','credit_after_refusals','mortgage_refinance','unknown'];\n let svc=String(r.service_hint||'').toLowerCase();\n if(classifiedSvc.indexOf(svc)<0){\n if(/\u0438\u043f\u043e\u0442\u0435\u043a|\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440|\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a/.test(blob))svc='mortgage_refinance';\n else if(/\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430|\u0438\u043f \u0438 \u043e\u043e\u043e|\u043e\u0431\u043e\u0440\u043e\u0442\u043d|\u0442\u0435\u043d\u0434\u0435\u0440\u043d|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a|\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442/.test(blob))svc='business_credit';\n else if(/\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432|\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a|\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a|\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d/.test(blob))svc='credit_after_refusals';\n else svc='credit_broker';\n }\n const termDefs=[['\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b','\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b'],['\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442','\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442'],['\u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443','\u0440\u0430\u0431\u043e\u0442\u0430 \u043f\u043e \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0443'],['\u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u0446\u0438','\u043a\u043e\u043d\u0441\u0443\u043b\u044c\u0442\u0430\u0446\u0438\u044f'],['\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432'],['\u043e\u0442\u043a\u0430\u0437\u0430\u0445 \u0431\u0430\u043d\u043a','\u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043a\u0430\u0437\u043e\u0432 \u0431\u0430\u043d\u043a\u043e\u0432'],['\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a','\u0441 \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0430\u043c\u0438'],['\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d','\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f'],['\u043f\u0430\u043a\u0435\u0442 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442','\u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043f\u0430\u043a\u0435\u0442\u043e\u043c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u043e\u0432'],['\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a','\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a\u0438\u0435 \u0433\u0430\u0440\u0430\u043d\u0442\u0438\u0438'],['\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440','\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435'],['\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a','\u0441\u043d\u0438\u0436\u0435\u043d\u0438\u0435 \u0441\u0442\u0430\u0432\u043a\u0438']];\n const seenT={}; const terms=[];\n termDefs.forEach(function(d){ if(blob.indexOf(d[0])>=0 && !seenT[d[1]]){seenT[d[1]]=1;terms.push(d[1]);} });\n const termsStr=(price?price:'')+((price&&terms.length)?'; ':'')+terms.join('; ');\n const strong=/\u043e\u0442\u043a\u0430\u0437\u043e\u0432|\u043f\u043b\u043e\u0445\u0430\u044f \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u043b\u043e\u0445\u043e\u0439 \u043a\u0440\u0435\u0434\u0438\u0442\u043d|\u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a|\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043e\u043f\u043b\u0430\u0442\u044b|\u043e\u043f\u043b\u0430\u0442\u0430 \u0437\u0430 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|\u0438\u043f \u0438 \u043e\u043e\u043e|\u0434\u043b\u044f \u0431\u0438\u0437\u043d\u0435\u0441\u0430|\u0431\u0438\u0437\u043d\u0435\u0441-\u043a\u0440\u0435\u0434\u0438\u0442|\u0431\u0430\u043d\u043a\u043e\u0432\u0441\u043a|\u0440\u0435\u0444\u0438\u043d\u0430\u043d\u0441\u0438\u0440/.test(blob);\n let cs=75; if(price)cs+=4; if(terms.length>=2)cs+=4; if(description.length>40)cs+=2; if(cs>85)cs=85;\n let ci=35; if(price||terms.length)ci=45; if(strong)ci=(price&&terms.length)?55:50;\n let comp=0; [title,price,seller,description,s2(r.profile_url)].forEach(function(x){if(s2(x))comp++;});\n const qs=comp>=5?82:(comp>=4?78:(comp>=3?74:70));\n const company=s2(r.competitor_name)||seller||s2(r.profile_name)||'';\n const offer=title||s2(r.interest_topic);\n const need=s2(r.probable_need);\n const reason='Avito-\u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u0430: '+(offer||'\u043e\u0444\u0444\u0435\u0440')+(price?(' ('+price+')'):'')+(terms.length?('; '+terms.slice(0,3).join(', ')):'')+'. \u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043e\u0444\u0444\u0435\u0440, \u0446\u0435\u043d\u0443 \u0438 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u043a\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043e\u0432 \u0438 \u0440\u0435\u043a\u043b\u0430\u043c\u043d\u044b\u0445 \u0443\u0433\u043b\u043e\u0432 (\u043f\u043b\u043e\u0449\u0430\u0434\u043a\u0430 Avito/classifieds). \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: monitor.';\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company, service_type:svc, region:s2(r.region_hint)||loc, competitor_strength:cs, lead_signal_score:1, content_idea_score:ci, quality_score:qs, needs_manual_review:false, status:'analyzed', detected_need:need, offer_text:offer, terms:termsStr, contact:cleanContact(r.contact_public), reason:reason };\n}\nfunction classifyDeterministic(r){\n // \u00a72.1 Stage A / \u00a79.1 system-event pre-gate: Telegram service messages (channel rename/photo/pin) are\n // hard-skipped to skipped_log BEFORE any Claude call ($0). Service is NOT derived from a changed channel title.\n var _sysTxt=str(r.text_context)+' '+str(r.comment_text);\n if(low(r.record_type_hint)==='system_event'||/channel name was changed to|\u043a\u0430\u043d\u0430\u043b \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d|(\u0441\u043c\u0435\u043d\u0438\u043b|\u0438\u0437\u043c\u0435\u043d\u0438\u043b)\\s+\u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\\s+\u043a\u0430\u043d\u0430\u043b\u0430|(\u0441\u043c\u0435\u043d\u0438\u043b|\u0438\u0437\u043c\u0435\u043d\u0438\u043b|\u043e\u0431\u043d\u043e\u0432\u0438\u043b)\\s+\u0444\u043e\u0442\u043e\\s+\u043a\u0430\u043d\u0430\u043b\u0430|channel (was )?created|\u043a\u0430\u043d\u0430\u043b \u0441\u043e\u0437\u0434\u0430\u043d/i.test(_sysTxt)){\n return { is_irrelevant:true, deterministic_needs_llm:false, entity_type:'irrelevant', route:'skipped_log', recommended_action:'ignore', company_name:'', service_type:'unknown', region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:1, quality_score:1, needs_manual_review:false, status:'skipped', detected_need:'', offer_text:'', contact:'', reason:'\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u043e\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u0435 Telegram (\u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435/\u0441\u043c\u0435\u043d\u0430 \u0444\u043e\u0442\u043e/\u0437\u0430\u043a\u0440\u0435\u043f) \u2014 hard-skip \u0434\u043e Claude, $0. \u0421\u0435\u0440\u0432\u0438\u0441 \u043d\u0435 \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0438\u0437 \u043d\u043e\u0432\u043e\u0433\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u043a\u0430\u043d\u0430\u043b\u0430.', skip_reason:'telegram_service_message', is_system_event:true };\n }\n const rth=low(r.record_type_hint), tpt=low(r.touchpoint_type), lit=low(r.lead_intent_hint);\n const urg=low(r.urgency_hint), temp=low(r.lead_temperature);\n const compRelated=(r.competitor_related===true||low(r.competitor_related)==='true');\n const svcRaw=low(r.service_hint);\n const text=str(r.text_context)+' '+str(r.comment_text)+' '+str(r.interest_topic)+' '+str(r.probable_need);\n const contact=cleanContact(r.contact_public);\n const hasContact=contact!=='';\n const hasInsight=(str(r.comment_text)!=='');\n const allowedSvc=['secured_auto_loan','secured_real_estate_loan','pts_loan','refinancing','mortgage_adjacent','generic_lending','credit_broker','consumer_credit'];\n function svcNorm(def){ if(allowedSvc.indexOf(svcRaw)>=0)return svcRaw; const t=text.toLowerCase(); if(t.indexOf('\u043f\u0442\u0441')>=0||t.indexOf('pts')>=0)return 'pts_loan'; if(t.indexOf('\u0431\u0440\u043e\u043a\u0435\u0440')>=0)return 'credit_broker'; return def||'unknown'; }\n function company(){ const c=str(r.competitor_name); if(c)return c; const p=str(r.profile_name); if(p)return p; const h=host(r.source_url||r.profile_url); if(h)return h; return ''; }\n // 1. irrelevant -> skipped_log (pre-Claude, $0)\n if(rth==='irrelevant'||tpt==='irrelevant_source'||tpt==='weak_market_noise'){\n return { is_irrelevant:true, deterministic_needs_llm:false, entity_type:'irrelevant', route:'skipped_log', recommended_action:'ignore', company_name:'', service_type:'unknown', region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:1, quality_score:1, needs_manual_review:false, status:'skipped', detected_need:'', offer_text:'', contact:'' };\n }\n // 2. competitor activity -> monitor_queue\n if(rth==='competitor_activity'||(compRelated&&tpt.indexOf('competitor')>=0)){\n const tc0=str(r.text_context);\n if(low(r.source_type)==='classified'&&low(r.platform)==='avito'&&/^avito \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u0435:/i.test(tc0)){ return classifiedCompetitorDet(r,tc0); }\n const clear=/\u20bd|\u0440\u0443\u0431|\u0446\u0435\u043d\u0430|\u0441\u0442\u0430\u0432\u043a|%|\u043e\u0444\u0444\u0435\u0440|offer|\u0443\u0441\u043b\u0443\u0433|\u043f\u043e\u043c\u043e\u0449\u044c|\u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d|\u043a\u0430\u043d\u0430\u043b|\u043a\u0435\u0439\u0441|usp|\u043f\u0440\u0435\u0438\u043c\u0443\u0449\u0435\u0441\u0442\u0432/i.test(text);\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company(), service_type:svcNorm('generic_lending'), region:str(r.region_hint), competitor_strength:clear?78:70, lead_signal_score:1, content_idea_score:hasInsight?30:1, quality_score:clear?80:72, needs_manual_review:false, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n }\n // 4. review source (handle before generic source_candidate). DEC-087: ONLY a NAMED competitor\n // (competitor_name set) is treated as a strong competitor -> monitor_queue. A generic category/\n // listing/review directory (e.g. a Zoon \"\u043a\u0440\u0435\u0434\u0438\u0442\u043d\u044b\u0435 \u0431\u0440\u043e\u043a\u0435\u0440\u044b \u0432 \u041c\u043e\u0441\u043a\u0432\u0435\" category page) is a CONTENT/\n // monitoring source -> content_queue, content_idea, competitor_strength capped (<=45), no \"strong competitor\".\n if(tpt==='review_source'){\n const specificCompetitor=(str(r.competitor_name)!=='');\n if(specificCompetitor){\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'competitor', route:'monitor_queue', recommended_action:'monitor', company_name:company(), service_type:svcNorm('generic_lending'), region:str(r.region_hint), competitor_strength:70, lead_signal_score:1, content_idea_score:35, quality_score:68, needs_manual_review:false, status:'analyzed', detected_need:str(r.probable_need), offer_text:str(r.interest_topic), contact:contact };\n }\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'content_idea', route:'content_queue', recommended_action:'create_content', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:(compRelated?45:1), lead_signal_score:1, content_idea_score:70, quality_score:68, needs_manual_review:true, status:'analyzed', detected_need:str(r.probable_need), offer_text:str(r.interest_topic), contact:contact };\n }\n // 3. source candidate / market signal -> content_queue\n if(rth==='market_signal'&&tpt==='source_candidate'){\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'content_idea', route:'content_queue', recommended_action:'create_content', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:55, quality_score:52, needs_manual_review:true, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n }\n // 5. question / objection / hot pattern -> results (with contact) or review_queue\n if(rth==='question_objection'||temp==='hot'||lit==='high'||urg==='high'){\n if(hasContact){\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'lead_signal', route:'results', recommended_action:'contact', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:85, content_idea_score:40, quality_score:80, needs_manual_review:false, status:'analyzed', detected_need:str(r.probable_need)||str(r.interest_topic), offer_text:str(r.interest_topic), contact:contact };\n }\n return { is_irrelevant:false, deterministic_needs_llm:false, entity_type:'lead_signal', route:'review_queue', recommended_action:'investigate', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:75, content_idea_score:40, quality_score:70, needs_manual_review:true, status:'analyzed', detected_need:str(r.probable_need)||str(r.interest_topic), offer_text:str(r.interest_topic), contact:contact };\n }\n // 6. fallback unknown -> review_queue content_idea (classifiable but uncertain -> ask Claude when available)\n return { is_irrelevant:false, deterministic_needs_llm:true, entity_type:'content_idea', route:'review_queue', recommended_action:'investigate', company_name:'', service_type:svcNorm('unknown'), region:str(r.region_hint), competitor_strength:1, lead_signal_score:1, content_idea_score:40, quality_score:40, needs_manual_review:true, status:'analyzed', detected_need:'', offer_text:str(r.interest_topic), contact:contact };\n}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst r=$json;\nconst cfg=$('Set Analyzer Config').first().json;\nconst llm_enrichment=(cfg.llm_enrichment===true);\nconst llm_test=(cfg.llm_enrichment_test_mode===true);\nconst testIdx=Array.isArray(cfg.llm_test_batch_indexes)?cfg.llm_test_batch_indexes.map(Number):[];\nconst now=moscowIsoNow();\nlet combined=str(r.text_context);\nif(str(r.comment_text))combined+=' || \u041a\u041e\u041c\u041c\u0415\u041d\u0422\u0410\u0420\u0418\u0419: '+str(r.comment_text);\nif(str(r.interest_topic))combined+=' || \u0422\u0415\u041c\u0410: '+str(r.interest_topic);\nif(str(r.probable_need))combined+=' || \u0412\u0415\u0420\u041e\u042f\u0422\u041d\u0410\u042f \u041f\u041e\u0422\u0420\u0415\u0411\u041d\u041e\u0421\u0422\u042c: '+str(r.probable_need);\nconst det=classifyDeterministic(r);\n// LLM gate (DEC-082): call Claude only for non-irrelevant records when enrichment is ON, or when det is uncertain.\n// llm_enrichment_test_mode (Part C): restrict Claude to the listed fixture batch_indexes only (e.g. [1,7,11,12]).\n// MASTER LLM KILL SWITCH (DEC-119): llm_enabled=false guarantees zero Claude calls \u2014 uncertain records\n// (deterministic_needs_llm=true) are NOT escalated to Claude; they go deterministically to review_queue\n// with parse_method=deterministic_uncertain_no_llm and estimated_analysis_cost_usd=0 (manual review).\nconst llm_enabled=(cfg.llm_enabled===true);\n// PRODUCTION LLM RUNTIME GUARD (\u00a71): the Claude node is ENABLED in JSON; a call proceeds ONLY when EVERY\n// condition holds. Any failure => deterministic path ($0). llm_enabled=false guarantees zero Claude calls.\nfunction _tru(v){return v===true||String(v).toLowerCase()==='true';}\nconst _apprReq=(cfg.require_llm_approval!==false);\nconst _apprOk=(!_apprReq)||(str(cfg.llm_approval_token)!==''&&str(cfg.llm_approval_token)===str(cfg.expected_llm_approval_token));\nconst _qualOk=(['degraded','quarantined'].indexOf(str(r.quality_status).toLowerCase())<0)&&(str(r.review_status).toLowerCase()!=='pending');\nconst _notDone=(str(r.analysis_status).toLowerCase()!=='analyzed'&&str(r.analysis_status).toLowerCase()!=='done');\nconst _notCancelled=!_tru(cfg.request_cancelled)&&!_tru(r.request_cancelled);\nconst _perCall=Number(cfg.estimated_llm_cost_per_call_usd);const _estPerCall=(isFinite(_perCall)&&_perCall>0)?_perCall:0.01;\nconst _budget=Number(cfg.llm_budget_usd);const _budgetOk=(!isFinite(_budget)||_budget<=0)?true:(_estPerCall<=_budget);\nconst _guardOk=llm_enabled&&_apprOk&&_qualOk&&_notDone&&_notCancelled&&_budgetOk;\nconst llm_guard_reason=(!llm_enabled)?'llm_disabled':(!_apprOk?'approval_token_invalid':(!_qualOk?'record_not_quality_eligible':(!_notDone?'already_analyzed':(!_notCancelled?'request_cancelled':(!_budgetOk?'over_llm_budget':'ok')))));\nlet call_claude;\nif(!_guardOk){\n call_claude=false;\n} else if(llm_test){\n call_claude=(!det.is_irrelevant)&&(testIdx.length>0?(testIdx.indexOf(Number(r.batch_index))>=0):true);\n} else {\n call_claude=(!det.is_irrelevant)&&(llm_enrichment===true||det.deterministic_needs_llm===true);\n}\nconst llm_blocked_uncertain=(!_guardOk)&&(!det.is_irrelevant)&&(det.deterministic_needs_llm===true);\nreturn [{ json: Object.assign({}, r, { parsed_at:now, analyzer_text:combined.substring(0,3500), det:det, is_irrelevant:det.is_irrelevant, deterministic_needs_llm:det.deterministic_needs_llm, call_claude:call_claude, llm_enabled:llm_enabled, llm_guard_ok:_guardOk, llm_guard_reason:llm_guard_reason, estimated_llm_cost_per_call_usd:_estPerCall, llm_blocked_uncertain:llm_blocked_uncertain, analysis_mode:cfg.analysis_mode||'deterministic_first', llm_enrichment:llm_enrichment, llm_enrichment_test_mode:llm_test }) }];"
}
},
{
"id": "a8000000-0000-0000-0000-0000000000ii",
"name": "IF Call Claude?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1420,
440
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose"
},
"conditions": [
{
"id": "a8-if-callclaude-01",
"leftValue": "={{ $json.call_claude }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
}
}
},
{
"id": "a8000000-0000-0000-0000-0000000000sk",
"name": "Build Deterministic Row",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1640,
280
],
"parameters": {
"jsCode": "// DETERMINISTIC-FIRST row builder (DEC-082). Handles BOTH:\n// - irrelevant records -> skipped_log (parse_method=deterministic_irrelevant_skip)\n// - obvious classifiable records routed WITHOUT Claude (parse_method=deterministic_pre_route)\n// Claude is NOT called for these rows ($0). 35 business fields on every path.\nfunction str(v){return v==null?'':String(v).trim();}\nfunction moscowIsoNow(){const m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst src=$('Prepare Record').first().json;\nconst det=src.det||{};\n// --- Stage C Closure Patch 3: canonical source lineage (same chain WF16 uses to key source_health) ---\nfunction __lineage(pm, route){\n var srid=str(src.source_run_id)||str(src.run_id)||str(src.agent_request_id);\n var __cfg=$('Set Analyzer Config').first().json||{};\n var dmode=(str(src.data_mode)||str(__cfg.data_mode)||'live').toLowerCase();\n var p=String(pm||'').toLowerCase(), r=String(route||'').toLowerCase();\n var pending=(p==='deterministic_uncertain_no_llm');\n var review=pending?'pending':((str(src.review_status)||'confirmed').toLowerCase());\n var wfQ=(r==='technical_errors'||r==='skipped_log'||p.indexOf('irrelevant')>=0||p==='technical_error')?'quarantined':((pending||p==='deterministic_fallback_after_llm_fail')?'degraded':'healthy');\n var srcQ=String(src.quality_status||'').toLowerCase();\n var rank={healthy:0,degraded:1,quarantined:2};\n var quality=(rank[srcQ]!==undefined&&rank[srcQ]>rank[wfQ])?srcQ:wfQ;\n var reportFalse=(src.report_eligible===false||String(src.report_eligible).toLowerCase()==='false');\n var reportElig=!(quality==='quarantined'||pending||reportFalse);\n return {source_run_id:srid,data_mode:dmode,quality_status:quality,review_status:review,report_eligible:reportElig,quality_flags:str(src.quality_flags)};\n}\n// Note (DEC-086): C2 test filtering happens in Filter & Select Records (pre-loop). This node never\n// returns [] -- non-target test records never reach it -- so loop continuation is never broken.\nconst now=moscowIsoNow();\nconst irrel=(det.is_irrelevant===true);\nconst llmBlocked=(src.llm_blocked_uncertain===true);\nconst parse_method=irrel?'deterministic_irrelevant_skip':(llmBlocked?'deterministic_uncertain_no_llm':'deterministic_pre_route');\nconst processing_status=irrel?'business_skip':'parsed_success';\nconst reason=irrel\n ?'\u0417\u0430\u043f\u0438\u0441\u044c \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u0430 \u043d\u0435\u0440\u0435\u043b\u0435\u0432\u0430\u043d\u0442\u043d\u043e\u0439 \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043f\u0440\u0438\u0451\u043c\u0430 (record_type_hint=irrelevant \u0438\u043b\u0438 touchpoint_type=irrelevant_source/weak_market_noise). Claude \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u2014 \u043d\u0443\u043b\u0435\u0432\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c.'\n :(llmBlocked\n ?'Cost-control \u0440\u0435\u0436\u0438\u043c (DEC-119): llm_enabled=false \u2014 Claude \u043e\u0442\u043a\u043b\u044e\u0447\u0451\u043d, \u043d\u0435\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u041d\u0415 \u044d\u0441\u043a\u0430\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0432 LLM. \u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435\u0443\u0432\u0435\u0440\u0435\u043d\u043d\u0430\u044f (deterministic_needs_llm=true) \u2192 review_queue, \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c. \u0421\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437\u0430 $0. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: investigate.'\n :(str(det.reason)?str(det.reason):'\u0414\u0435\u0442\u0435\u0440\u043c\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0430\u043c \u043f\u0440\u0438\u0451\u043c\u0430 raw_market_records (deterministic_first, llm_enrichment=false): \u0437\u0430\u043f\u0438\u0441\u044c \u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u043e \u043a\u043b\u0430\u0441\u0441\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u043c\u0430 \u0431\u0435\u0437 Claude. \u041c\u0430\u0440\u0448\u0440\u0443\u0442: '+(det.route||'')+'. \u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435: '+(det.recommended_action||'')+'. Claude \u043d\u0435 \u0432\u044b\u0437\u044b\u0432\u0430\u043b\u0441\u044f \u2014 \u043d\u0443\u043b\u0435\u0432\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c.'));\nconst __linDet=__lineage(parse_method, det.route||'skipped_log');\nreturn [{ json: {\n created_at:now, source_type:str(src.source_type), platform:str(src.platform), source_url:str(src.source_url),\n parsed_at:str(src.parsed_at)||now, published_at:str(src.published_at), freshness_status:'unknown',\n entity_type:det.entity_type||'irrelevant', company_name:str(det.company_name), profile_name:str(src.profile_name), profile_url:str(src.profile_url),\n region:str(det.region)||str(src.region_hint), service_type:det.service_type||'unknown', offer_text:str(det.offer_text), terms:str(det.terms), contact_public:str(det.contact),\n text_context:str(src.analyzer_text||src.text_context), detected_need:str(det.detected_need),\n competitor_strength:det.competitor_strength||1, lead_signal_score:det.lead_signal_score||1, content_idea_score:det.content_idea_score||1, quality_score:det.quality_score||1,\n reason:reason,\n recommended_action:det.recommended_action||'ignore', status:det.status||(irrel?'skipped':'analyzed'), processing_status:processing_status,\n parse_method:parse_method, parse_error:'', raw_response_preview:'',\n route:det.route||'skipped_log', needs_manual_review:(det.needs_manual_review===true), repair_used:false, repair_status:'',\n run_id:str(src.run_id), batch_index:src.batch_index||0,\n source_run_id:__linDet.source_run_id, data_mode:__linDet.data_mode, quality_status:__linDet.quality_status, review_status:__linDet.review_status, report_eligible:__linDet.report_eligible, quality_flags:__linDet.quality_flags\n}}];"
}
},
{
"id": "a8000000-0000-0000-0000-0000000000bp",
"name": "Build Primary Claude Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1640,
600
],
"parameters": {
"jsCode": "// \u00a72.1/\u00a75 WF08 llm_primary \u2014 Claude is the PRIMARY semantic classifier (semantic-v2). POST_EVIDENCE overrides hints.\n// HARD GUARD (DEC-119): no record reaches Claude while llm_enabled=false.\nconst cfgGuard=$('Set Analyzer Config').first().json;\nif(cfgGuard.llm_enabled!==true){throw new Error('WF08 LLM guard: llm_enabled=false (cost-control) \u2014 Claude blocked. Set llm_enabled=true with explicit operator approval to run llm_primary.');}\nconst r=$('Prepare Record').first().json;\nfunction cap(s,n){return (s==null?'':String(s)).substring(0,n);}\nconst POST_EVIDENCE={ post_text:cap(r.analyzer_text||r.text_context||'',1600), post_url:r.post_url||r.source_url||'', listing_title:r.title||'', published_at:r.published_at||'' };\nconst SOURCE_METADATA={ platform:r.platform||'', source_type:r.source_type||'', channel_or_profile_name:r.profile_name||'', profile_url:r.profile_url||'' };\nconst UPSTREAM_HINTS={ record_type_hint:r.record_type_hint||'', service_hint:r.service_hint||'', touchpoint_type:r.touchpoint_type||'', _note:'WEAK PRIORS ONLY \u2014 may be wrong; POST_EVIDENCE overrides these.' };\nconst ALLOWED_ENUMS={ record_type:[\"competitor_activity\", \"market_signal\", \"audience_question\", \"audience_objection\", \"audience_complaint\", \"buying_intent\", \"lead_signal\", \"source_candidate\", \"irrelevant\", \"system_event\", \"unknown\"], entity_type:[\"competitor\", \"market_signal\", \"content_idea\", \"audience_signal\", \"lead\", \"source_candidate\", \"irrelevant\", \"system_event\", \"unknown\"], activity_subtype:[\"direct_offer\", \"owned_media_content\", \"affiliate_program\", \"product_explainer\", \"educational_offer\", \"client_case\", \"price_change\", \"service_change\", \"channel_profile\", \"other\"], service:[\"credit_brokerage\", \"credit_after_refusals\", \"credit_history_consulting\", \"pts_loan\", \"secured_financing\", \"real_estate_secured_loan\", \"business_credit\", \"mortgage_brokerage\", \"mortgage_refinance\", \"auto_credit\", \"auto_lease_refinance\", \"debt_refinancing\", \"debt_consolidation\", \"bankruptcy_advisory\", \"business_credit_rehabilitation\", \"credit_legal_consulting\", \"consumer_credit\", \"bank_guarantee\", \"unknown\"] };\nconst OUTPUT_SCHEMA={ schema_version:'semantic-v2', record_type:'', entity_type:'', activity_subtype:null, competitor_related:false, competitor_name:null, service_primary:'unknown', service_secondary:[], market_signal_subtype:null, content_topics:[], pain_tags:[], offer_text:'', offer_terms:{amount_min:null,amount_max:null,currency:'RUB',price_text:null,rate_text:null,speed_text:null,conditions:[],documents_not_required:[]}, cta:null, quoted_claims:[], negated_claims:[], hard_skip:false, skip_reason:null, confidence_score:0, confidence_reasons:[], semantic_flags:[], evidence:[] };\nconst system=\"You are the PRIMARY semantic classifier for a Russian secured-lending market-intelligence system (Moscow/MO: PTS / auto / real-estate / business collateral, brokerage, refinancing, credit history). You are NOT a text enricher: you DECIDE the semantic class of each record from its own evidence.\\\\nYou cannot browse, fetch, open or verify anything. Use ONLY the provided record.\\\\nPRIORITY OF EVIDENCE: POST_EVIDENCE (the actual post/listing text + URL) is the source of truth. SOURCE_METADATA (platform, channel/profile name) and UPSTREAM_HINTS (record_type_hint/service_hint from the connector) are WEAK PRIORS ONLY and are frequently wrong. If POST_EVIDENCE contradicts a hint, OVERRIDE the hint.\\\\nDo NOT infer an offer or a competitor from a channel title or a search query alone \\u2014 require post-level evidence (a pitch, price, terms, CTA, commission, case, or owned-media funnel).\\\\nRULES: (1) A direct competitor offer / partner-referral program / owned-media expertise funnel
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.
googleApihttpHeaderAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
08 - Touchpoint Analyzer. Uses googleSheets, httpRequest, executeWorkflowTrigger. Event-driven trigger; 25 nodes.
Source: https://github.com/CodeVinci8/vinci-ai-pilot/blob/main/n8n/workflows/08_touchpoint_analyzer.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.
NDA Import. Uses httpRequest, googleSheets, jira, executeWorkflowTrigger. Event-driven trigger; 30 nodes.
12 - Market Intelligence Report Builder (Stakeholder v0.3). Uses googleSheets, httpRequest, executeWorkflowTrigger. Event-driven trigger; 27 nodes.
Google Maps Email Scraper Template. Uses removeDuplicates, splitInBatches, httpRequest, splitOut. Event-driven trigger; 26 nodes.
Email Scapper. Uses httpRequest, googleSheets, executeWorkflowTrigger. Event-driven trigger; 26 nodes.
This workflow takes a list of Google Maps search queries, extracts candidate business website URLs from the search results, visits each site, scrapes email addresses from the page content, and appends