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.
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
ITHome比賽進程. Uses httpRequest, googleSheets, executeWorkflowTrigger, n8n. Event-driven trigger; 25 nodes.