AutomationFlowsData & Sheets › Approved Candidates Runner

Approved Candidates Runner

06 - Approved Candidates Runner. Uses googleSheets. Event-driven trigger; 10 nodes.

Event trigger★★★★☆ complexity10 nodesGoogle Sheets
Data & Sheets Trigger: Event Nodes: 10 Complexity: ★★★★☆ Added:

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "06 - Approved Candidates Runner",
  "nodes": [
    {
      "id": "a6000000-0000-0000-0000-0000000000n1",
      "name": "Overview Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -380,
        -180
      ],
      "parameters": {
        "content": "## 06 \u2014 \u0417\u0430\u043f\u0443\u0441\u043a \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u043d\u044b\u0445 \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u043e\u0432 (Approved Candidates Runner)\n\n\u041c\u043e\u0441\u0442 \u043c\u0435\u0436\u0434\u0443 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435\u043c (Workflow 05) \u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0435\u043c URL (Workflow 04).\n\n\u0427\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442:\n1. \u0420\u0443\u0447\u043d\u043e\u0439 \u0441\u0442\u0430\u0440\u0442.\n2. \u0427\u0438\u0442\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 url_candidates.\n3. \u0427\u0438\u0442\u0430\u0435\u0442 \u0432\u043a\u043b\u0430\u0434\u043a\u0443 url_registry (\u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0440\u0430\u0432\u0434\u044b \u0434\u0435\u0434\u0443\u043f\u0430) \u041f\u041e\u0412\u0422\u041e\u0420\u041d\u041e, \u0432 \u043c\u043e\u043c\u0435\u043d\u0442 \u043f\u0440\u043e\u0433\u043e\u043d\u0430.\n4. \u0417\u0430\u043d\u043e\u0432\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0443\u0435\u0442 candidate_url \u0442\u0435\u043c\u0438 \u0436\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438, \u0447\u0442\u043e Workflow 04/05, \u0438 \u0441\u0432\u0435\u0440\u044f\u0435\u0442 normalized_source_url \u0441 url_registry.\n5. \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u0442 \u0441\u0442\u0440\u043e\u043a\u0438: approval_status=approved \u0418 candidate_url \u043d\u0435 \u043f\u0443\u0441\u0442 \u0418 normalized_source_url \u041e\u0422\u0421\u0423\u0422\u0421\u0422\u0412\u0423\u0415\u0422 \u0432 url_registry. approval_status processed/duplicate/rejected/error \u2014 \u043f\u0440\u043e\u043f\u0443\u0441\u043a.\n6. \u0412\u0410\u0416\u041d\u041e: dedup_status / registry_status \u0432 url_candidates \u2014 \u044d\u0442\u043e \u043f\u043e\u0434\u0441\u043a\u0430\u0437\u043a\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f (advisory), \u0430 \u041d\u0415 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430. Workflow 06 \u041d\u0415 \u0434\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0438\u043c \u0438 \u043f\u0435\u0440\u0435\u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 url_registry. \u0415\u0441\u043b\u0438 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u043f\u043e\u0441\u0442\u0430\u0432\u0438\u043b unique/not_in_registry, \u043d\u043e URL \u0443\u0436\u0435 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u2014 \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f \u0441 \u043f\u0440\u0438\u0447\u0438\u043d\u043e\u0439 registry_recheck_duplicate.\n7. \u041f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0437\u0430\u0446\u0438\u044f: \u0441\u043d\u0430\u0447\u0430\u043b\u0430 direct_competitor, \u0437\u0430\u0442\u0435\u043c \u0432\u044b\u0448\u0435 confidence_score, \u0437\u0430\u0442\u0435\u043c \u043d\u0438\u0436\u0435 rank.\n8. \u0416\u0451\u0441\u0442\u043a\u0430\u044f \u043e\u0442\u0441\u0435\u0447\u043a\u0430 \u2014 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u043e\u0432 \u0437\u0430 \u043f\u0440\u043e\u0433\u043e\u043d.\n9. \u0413\u043e\u0442\u043e\u0432\u0438\u0442 \u0431\u0430\u0442\u0447 URL \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 Workflow 04 + \u0433\u043e\u0442\u043e\u0432\u044b\u0439 \u0431\u043b\u043e\u043a \u0434\u043b\u044f \u0432\u0441\u0442\u0430\u0432\u043a\u0438 \u0432 Set URL List.\n10. \u0423\u0437\u0435\u043b Mark Candidates Processed \u0412\u041a\u041b\u042e\u0427\u0401\u041d \u0438 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d: \u043e\u043d \u0441\u0442\u0430\u0432\u0438\u0442 approval_status=processed \u0422\u041e\u041b\u042c\u041a\u041e \u0434\u043b\u044f \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u043e\u0432, \u0447\u0435\u0439 normalized_source_url \u0443\u0436\u0435 \u043f\u043e\u044f\u0432\u0438\u043b\u0441\u044f \u0432 url_registry (\u0442.\u0435. Workflow 04 \u0438\u0445 \u0440\u0435\u0430\u043b\u044c\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043b). \u0418\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e: \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u044b\u0439 \u043f\u0440\u043e\u0433\u043e\u043d \u043d\u0435 \u0442\u0440\u043e\u0433\u0430\u0435\u0442 \u0443\u0436\u0435 processed \u0438 \u041d\u0415 \u043f\u043e\u043c\u0435\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0435/\u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0435. \u0421\u0432\u0435\u0436\u0435\u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0435 \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u044b \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f approved \u0434\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0447\u0435\u0440\u0435\u0437 url_registry.\n\n\u0410\u0433\u0440\u0435\u0433\u0430\u0442\u043e\u0440/\u043a\u0430\u0442\u0430\u043b\u043e\u0433/\u043c\u0430\u0440\u043a\u0435\u0442\u043f\u043b\u0435\u0439\u0441/\u0441\u043e\u0446\u0441\u0435\u0442\u044c/\u043c\u0435\u0434\u0438\u0430 \u043c\u043e\u0436\u043d\u043e \u0432\u044b\u0431\u0440\u0430\u0442\u044c, \u0415\u0421\u041b\u0418 approval_status=approved, \u043d\u043e \u0432 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u043c \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435 \u0441\u0442\u0430\u0432\u0438\u0442\u0441\u044f \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435: candidate_type \u043d\u0435 direct_competitor; \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u0434 Workflow 04.\n\n\u0427\u0415\u0413\u041e \u041d\u0415 \u0414\u0415\u041b\u0410\u0415\u0422 (\u0430\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u043d\u043e):\n- \u041d\u0415 \u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0435\u0442 \u043b\u043e\u0433\u0438\u043a\u0443 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f, \u041d\u0415 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Apify, \u041d\u0415 \u0438\u0449\u0435\u0442 \u043d\u043e\u0432\u044b\u0435 URL.\n- \u041d\u0415 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442 Firecrawl/Claude \u043d\u0430\u043f\u0440\u044f\u043c\u0443\u044e \u2014 \u0430\u043d\u0430\u043b\u0438\u0437 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 Workflow 04.\n- \u041d\u0415 Telegram, \u041d\u0415 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435, \u041d\u0415 \u0430\u0432\u0442\u043e-\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435. \u041f\u0435\u0440\u0435\u0432\u043e\u0434 \u0432 processed \u2014 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044e \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u044f \u0432 url_registry (\u043d\u0435 \u043f\u043e \u0444\u0430\u043a\u0442\u0443 \u0432\u044b\u0431\u043e\u0440\u0430).\n\nv0.1: \u0440\u0443\u0447\u043d\u0430\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0430 (manual handoff). \u041f\u043e\u0442\u043e\u043a: Workflow 05 \u2192 url_candidates \u2192 \u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u0435 \u2192 Workflow 06 (recheck url_registry) \u2192 (\u0440\u0443\u0447\u043d\u0430\u044f \u0432\u0441\u0442\u0430\u0432\u043a\u0430) \u2192 Workflow 04 \u2192 monitor_queue.",
        "height": 560,
        "width": 560,
        "color": 4
      }
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000n2",
      "name": "Setup and Test Note RU",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        220,
        -180
      ],
      "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 workflow (active=false).\n2. \u0412\u043a\u043b\u0430\u0434\u043a\u0438 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c: url_candidates (26 \u043a\u043e\u043b\u043e\u043d\u043e\u043a), url_registry (10 \u043a\u043e\u043b\u043e\u043d\u043e\u043a). \u041d\u043e\u0432\u044b\u0445 \u0432\u043a\u043b\u0430\u0434\u043e\u043a \u041d\u0415 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f.\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 (ID \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b):\n   - Read url_candidates \u2192 Google Sheets - Marketing Scout Service Account\n   - Read url_registry \u2192 \u0442\u043e\u0442 \u0436\u0435 \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\n   - Mark Candidates Processed (confirmed in url_registry) \u2192 \u0442\u043e\u0442 \u0436\u0435 \u043a\u0440\u0435\u0434\u0435\u043d\u0448\u043b\n4. \u041d\u0430 \u0432\u0441\u0435\u0445 Google Sheets \u043d\u043e\u0434\u0430\u0445 \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0440\u0435\u0430\u043b\u044c\u043d\u044b\u0439 Spreadsheet ID (\u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c PASTE_SPREADSHEET_ID_HERE).\n5. \u041d\u0438\u043a\u0430\u043a\u0438\u0445 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 API-\u043a\u043b\u044e\u0447\u0435\u0439 \u0437\u0434\u0435\u0441\u044c \u043d\u0435\u0442 (\u043d\u0435\u0442 Apify/Firecrawl/Claude).\n\n\u0420\u0435\u0442\u0435\u0441\u0442 (\u043f\u043e\u0441\u043b\u0435 \u043f\u0430\u0442\u0447\u0430 registry recheck):\n\u0430) \u0421\u0442\u0430\u0440\u044b\u0439 \u0434\u0443\u0431\u043b\u0438\u043a\u0430\u0442, \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u043f\u0435\u0440\u0430\u0442\u043e\u0440\u043e\u043c approved/unique/not_in_registry, \u043d\u043e \u0443\u0436\u0435 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0432 url_registry \u2192 \u0414\u041e\u041b\u0416\u0415\u041d \u0431\u044b\u0442\u044c \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d \u0441 \u043f\u0440\u0438\u0447\u0438\u043d\u043e\u0439 registry_recheck_duplicate (\u041d\u0415 \u0432\u044b\u0431\u0440\u0430\u043d).\n\u0431) \u041d\u043e\u0432\u044b\u0439 direct_competitor \u0441 approval_status=approved, \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u041d\u0415\u0422 \u0432 url_registry \u2192 \u0414\u041e\u041b\u0416\u0415\u041d \u0431\u044b\u0442\u044c \u0432\u044b\u0431\u0440\u0430\u043d.\n\n\u041f\u0435\u0440\u0432\u044b\u0439/\u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u0442\u0435\u0441\u0442:\n1. \u0412 url_candidates \u043e\u0434\u043e\u0431\u0440\u0438\u0442\u044c \u041e\u0414\u041d\u0423 \u0441\u0442\u0440\u043e\u043a\u0443 direct_competitor: approval_status=approved, \u0437\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c approved_by/approved_at.\n2. Execute Workflow \u043e\u0434\u0438\u043d \u0440\u0430\u0437.\n3. \u041e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u044b\u0445\u043e\u0434 \u0443\u0437\u043b\u0430 Build Execution Summary & Handoff: selected_count, selected_urls, skipped (\u0441 reason_category), \u0433\u043e\u0442\u043e\u0432\u044b\u0439 \u0431\u043b\u043e\u043a workflow_04_set_url_list_block.\n4. \u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c URL (\u22645) \u0432 Workflow 04 \u2192 Set URL List, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Workflow 04 \u2192 \u043e\u0436\u0438\u0434\u0430\u0442\u044c monitor_queue + \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 url_registry.\n5. \u041f\u041e\u0421\u041b\u0415 \u043f\u0440\u043e\u0433\u043e\u043d\u0430 Workflow 04 (URL \u043f\u043e\u043f\u0430\u043b\u0438 \u0432 url_registry) \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Workflow 06: \u0443\u0437\u0435\u043b Mark Candidates Processed \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438 \u0438\u0434\u0435\u043c\u043f\u043e\u0442\u0435\u043d\u0442\u043d\u043e \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u0438\u0442 approval_status=processed \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043d\u043d\u044b\u043c \u043a\u0430\u043d\u0434\u0438\u0434\u0430\u0442\u0430\u043c. \u0420\u0443\u0447\u043d\u0430\u044f \u043f\u0440\u0430\u0432\u043a\u0430 \u043d\u0435 \u043d\u0443\u0436\u043d\u0430.\n\n\u041e\u0436\u0438\u0434\u0430\u0435\u043c\u043e: 0 \u0442\u0440\u0430\u0442 \u0432 \u0441\u0430\u043c\u043e\u043c 06; \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u2014 \u0442\u043e\u043b\u044c\u043a\u043e Workflow 04 (\u22645 URL).",
        "height": 560,
        "width": 560,
        "color": 4
      }
    },
    {
      "id": "a6000000-0000-0000-0000-000000000002",
      "name": "Manual Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        120,
        420
      ],
      "parameters": {}
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000cf",
      "name": "Set Runner Config",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        600
      ],
      "parameters": {
        "jsCode": "// Runner configuration. Edit runner_mode to switch selection behavior.\n//   first_pass_domain_diversity (DEFAULT) \u2014 max 1 selected URL per normalized domain per run.\n//   deep_domain_analysis            \u2014 allow multiple URLs per domain (cap 3/domain/run), explicit.\n// Both modes always keep: max_per_run=5, url_registry runtime recheck, exact normalized-URL dedup,\n// manual handoff to Workflow 04, no auto-call, no auto-mark processed.\nconst runner_mode = 'first_pass_domain_diversity';\nreturn [{ json: { runner_mode: runner_mode } }];"
      }
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000rd",
      "name": "Read url_candidates",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        340,
        420
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "url_candidates",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000rg",
      "name": "Read url_registry",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        540,
        420
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput",
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "url_registry",
          "mode": "name"
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000se",
      "name": "Select, Prioritize & Annotate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        760,
        420
      ],
      "parameters": {
        "jsCode": "// Single source of truth for selection. Reads url_candidates rows AND RE-READS url_registry\n// at runtime. candidate_url is RE-NORMALIZED with the SAME rules as Workflow 04/05 and verified\n// to NOT already exist in url_registry before selection. Editable dedup_status / registry_status\n// in url_candidates are advisory discovery-time hints only and are NOT trusted.\n//\n// runner_mode (from \"Set Runner Config\"):\n//   first_pass_domain_diversity (DEFAULT) \u2014 max 1 selected URL per normalized domain per run;\n//       extra approved same-domain URLs are skipped with reason_category=duplicate_domain_in_run.\n//   deep_domain_analysis \u2014 multiple approved URLs per domain allowed intentionally, capped at 3\n//       per domain per run; extras beyond 3 skipped with reason_category=domain_deep_limit;\n//       selected items carry a deep-mode warning.\n// BOTH modes ALWAYS enforce: url_registry runtime recheck, exact normalized-URL dedup, and the\n// hard cap max_per_run=5. url_registry semantics are UNCHANGED (full normalized URL, not domain).\nconst rows = $('Read url_candidates').all().map(i => i.json).filter(Boolean);\n\n// Re-read url_registry (source of truth for dedup) and build a set of normalized URLs.\nlet registry = [];\ntry { registry = $('Read url_registry').all().map(i => i.json).filter(Boolean); } catch (e) { registry = []; }\nconst regSet = new Set(registry.map(r => String(r.normalized_source_url || '').trim()).filter(Boolean));\n\n// Runner config (default first_pass_domain_diversity).\nlet runner_mode = 'first_pass_domain_diversity';\ntry {\n  const cfg = $('Set Runner Config').first().json || {};\n  const m = String(cfg.runner_mode || '').trim().toLowerCase();\n  if (m === 'deep_domain_analysis' || m === 'first_pass_domain_diversity') runner_mode = m;\n} catch (e) { /* keep default */ }\nconst DEEP = (runner_mode === 'deep_domain_analysis');\nconst PER_DOMAIN_CAP = DEEP ? 3 : 1; // deep: max 3 URLs/domain/run; first-pass: max 1/domain/run\n\nfunction pad(n){ return String(n).padStart(2, '0'); }\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nfunction moscowStamp(){var z=function(n){return String(n).padStart(2,'0');};var m=new Date(Date.now()+10800000);return m.getUTCFullYear()+z(m.getUTCMonth()+1)+z(m.getUTCDate())+'_'+z(m.getUTCHours())+z(m.getUTCMinutes())+z(m.getUTCSeconds());}\nconst now = moscowIsoNow();\nconst stamp = moscowStamp();\nconst run_id = 'approved_run_' + stamp;\nconst MAX = 5; // HARD CAP: max 5 approved URLs per run (both modes)\n\nfunction str(v){ return (v==null?'':String(v)).trim(); }\nfunction lc(v){ return str(v).toLowerCase(); }\n\n// URL normalizer \u2014 SAME rules as Workflow 04/05 (keys must match url_registry).\nfunction normalizeUrl(u){\n  u = String(u || '').trim();\n  if (!u) return '';\n  try {\n    const noFrag = u.split('#')[0];\n    const url = new URL(noFrag);\n    url.protocol = (url.protocol || '').toLowerCase();\n    url.hostname = (url.hostname || '').toLowerCase();\n    const drop = ['utm_source','utm_medium','utm_campaign','utm_term','utm_content','gclid','yclid','fbclid'];\n    for (const p of drop) url.searchParams.delete(p);\n    let path = url.pathname || '/';\n    if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1);\n    url.pathname = path;\n    return url.toString();\n  } catch (e) { return u; }\n}\n\n// Domain extractor from candidate_url \u2014 SAME rules as Workflow 05 (hostname, lowercase, strip leading www).\nfunction extractDomain(u){\n  let h = '';\n  try { h = new URL(String(u||'').trim()).hostname; } catch (e) { h = ''; }\n  if (!h) { const m = String(u||'').match(/^https?:\\/\\/([^\\/?#]+)/i); h = m ? m[1] : ''; }\n  h = h.toLowerCase();\n  if (h.indexOf('www.') === 0) h = h.slice(4);\n  return h;\n}\n\n// Root homepage detection (path empty or '/') \u2014 used as a priority tiebreaker.\nfunction isRootUrl(u){\n  // \u00a75 sandbox-safe (DEC-103): the n8n Code sandbox may not expose the URL global; new URL would throw and\n  // wrongly mark a real homepage (https://cashmotor.ru/, https://carcapital.ru/) as non-root. Use regex.\n  u=String(u||'').trim(); if(!u)return false;\n  var m=u.split('#')[0].split('?')[0].match(/^https?:\\/\\/[^\\/]+(\\/[^?#]*)?$/i);\n  if(!m)return false;\n  var p=m[1]||'/';\n  return (p===''||p==='/');\n}\n\n// Non-direct-competitor types are allowed when approved, but flagged with a warning.\nconst NON_DIRECT = ['aggregator','directory','marketplace','social','media_article'];\n\nconst evaluated = [];\nfor (const r of rows){\n  // Skip fully blank rows (alwaysOutputData can emit an empty item on an empty sheet).\n  if (!str(r.candidate_id) && !str(r.candidate_url)) continue;\n\n  const approval = lc(r.approval_status);\n  const url = str(r.candidate_url);\n  const ctype = lc(r.candidate_type);\n  const normalized = normalizeUrl(url); // re-normalized at runtime; stored field is NOT trusted\n  const domain = extractDomain(normalized || url); // re-derived from candidate_url, not from stored domain\n  const isRoot = isRootUrl(normalized || url);\n\n  let eligible = true; let skip_category = ''; let skip_reason = '';\n  if (approval === 'processed'){ eligible=false; skip_category='already_processed'; skip_reason='approval_status=processed (already handed off)'; }\n  else if (approval === 'duplicate' || approval === 'rejected' || approval === 'error'){ eligible=false; skip_category='duplicate_status'; skip_reason='approval_status=' + approval + ' (not eligible)'; }\n  else if (approval !== 'approved'){ eligible=false; skip_category='approval_status_not_approved'; skip_reason='approval_status=' + (approval||'empty') + ' (need approved)'; }\n  else if (url === ''){ eligible=false; skip_category='missing_candidate_url'; skip_reason='candidate_url empty'; }\n  else if (normalized !== '' && regSet.has(normalized)){ eligible=false; skip_category='registry_recheck_duplicate'; skip_reason='registry_recheck_duplicate: normalized_source_url already exists in url_registry (editable dedup_status/registry_status ignored)'; }\n\n  const warning = (eligible && NON_DIRECT.includes(ctype))\n    ? 'not_direct_competitor_optional_warning: candidate_type is not direct_competitor; review before Workflow 04.'\n    : '';\n\n  evaluated.push({ row:r, eligible, skip_category, skip_reason, warning, ctype, url, normalized, domain, isRoot,\n    confidence: parseInt(r.confidence_score)||0,\n    rank: parseInt(r.rank)||999 });\n}\n\n// Prioritize eligible: direct_competitor first, higher confidence, lower rank, then root homepage first.\nconst eligibleList = evaluated.filter(e => e.eligible);\neligibleList.sort((a,b)=>{\n  const ad = a.ctype==='direct_competitor'?0:1;\n  const bd = b.ctype==='direct_competitor'?0:1;\n  if (ad!==bd) return ad-bd;\n  if (b.confidence!==a.confidence) return b.confidence-a.confidence;\n  if (a.rank!==b.rank) return a.rank-b.rank;\n  const ar = a.isRoot?0:1; const br = b.isRoot?0:1;\n  return ar-br;\n});\n\n// Select in priority order, enforcing the per-domain cap (mode-dependent) and the overall hard cap.\nlet batch_index = 0;\nlet selectedCount = 0;\nconst domainCounts = {}; // domain -> selected count this run\nfor (let i=0;i<eligibleList.length;i++){\n  const e = eligibleList[i];\n  const dom = e.domain;\n  const dcount = dom ? (domainCounts[dom]||0) : 0;\n  if (dom && dcount >= PER_DOMAIN_CAP){\n    e._selected = false;\n    if (DEEP){\n      e._skip_category = 'domain_deep_limit';\n      e._skip_reason = 'domain_deep_limit: more than ' + PER_DOMAIN_CAP + ' approved URLs from domain ' + dom + ' in deep_domain_analysis mode; extra skipped.';\n    } else {\n      e._skip_category = 'duplicate_domain_in_run';\n      e._skip_reason = 'Same domain already selected in this run; use deep_domain_analysis mode for multi-page domain analysis.';\n    }\n    continue;\n  }\n  if (selectedCount >= MAX){\n    e._selected = false;\n    e._skip_category = 'over_limit';\n    e._skip_reason = 'over_limit (eligible but exceeds max_per_run=5; approve in a later run)';\n    continue;\n  }\n  e._selected = true; selectedCount++; batch_index++; e._batch_index = batch_index;\n  if (dom) domainCounts[dom] = (domainCounts[dom]||0) + 1;\n  if (DEEP) e._deep_warning = 'deep_domain_analysis mode: multiple URLs from same domain allowed intentionally.';\n}\n\nconst out = [];\nfor (const e of evaluated){\n  const r = e.row;\n  const isSel = (e._selected === true);\n  const skip_category = isSel ? '' : (e._skip_category || e.skip_category);\n  const skip = isSel ? '' : (e._skip_reason || e.skip_reason);\n  const warning = [e.warning || '', (isSel && e._deep_warning) ? e._deep_warning : ''].filter(Boolean).join(' | ');\n  out.push({ json: {\n    candidate_id: str(r.candidate_id),\n    discovery_request_id: str(r.discovery_request_id),\n    candidate_url: e.url,\n    normalized_source_url: e.normalized,\n    stored_normalized_source_url: str(r.normalized_source_url),\n    domain: e.domain,\n    is_root: e.isRoot,\n    candidate_type: str(r.candidate_type),\n    confidence_score: e.confidence,\n    rank: e.rank,\n    approval_status: (skip_category==='registry_recheck_duplicate') ? 'processed' : str(r.approval_status),\n    dedup_status: str(r.dedup_status),\n    registry_status: str(r.registry_status),\n    approved_by: str(r.approved_by),\n    approved_at: str(r.approved_at),\n    service_hint: str(r.service_hint),\n    region_hint: str(r.region_hint),\n    notes: str(r.notes),\n    runner_mode: runner_mode,\n    warning: warning,\n    _selected: isSel,\n    _confirm_processed: (skip_category==='registry_recheck_duplicate'), // approved + now in url_registry => WF04 processed it (safe, idempotent)\n    processed_update_payload: (skip_category==='registry_recheck_duplicate') ? { candidate_id: str(r.candidate_id), approval_status: 'processed' } : null,\n    _eligible: e.eligible,\n    _skip_category: skip_category,\n    _skip_reason: skip,\n    run_id: run_id,\n    batch_index: isSel ? e._batch_index : 0,\n    target_url: isSel ? e.url : '',\n    source_type: 'scraped_web',\n    platform: 'website',\n    source_candidate_id: str(r.candidate_id),\n    parsed_at: now,\n    source_note: 'approved_candidates_runner',\n    force_reprocess: false\n  }});\n}\nreturn out;"
      }
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000if",
      "name": "IF Confirmed Processed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        980,
        340
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a6-if-confirm-01",
              "leftValue": "={{ $json._confirm_processed }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000up",
      "name": "Mark Candidates Processed (confirmed in url_registry)",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1200,
        260
      ],
      "parameters": {
        "authentication": "serviceAccount",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "PASTE_SPREADSHEET_ID_HERE",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "url_candidates",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "candidate_id": "={{ $json.candidate_id }}",
            "approval_status": "={{ $json.approval_status }}",
            "notes": "={{ ($json.notes ? $json.notes + ' | ' : '') + 'Processed=confirmed in url_registry by WF06 run_id=' + $json.run_id + ' at ' + $now.toISO() }}"
          },
          "matchingColumns": [
            "candidate_id"
          ],
          "schema": []
        },
        "options": {}
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 5000
    },
    {
      "id": "a6000000-0000-0000-0000-0000000000su",
      "name": "Build Execution Summary & Handoff",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        980,
        540
      ],
      "parameters": {
        "jsCode": "// One summary item per run. Aggregates the annotated rows from the selection node.\nconst all = $('Select, Prioritize & Annotate').all().map(i => i.json);\nconst selected = all.filter(r => r._selected === true);\nconst skipped = all.filter(r => r._selected !== true);\nconst registry_recheck_duplicate = skipped.filter(r => r._skip_category === 'registry_recheck_duplicate').length;\nconst confirm_processed_count = all.filter(r => r._confirm_processed === true).length;\nconst run_id = (all[0] && all[0].run_id) || '';\nconst runner_mode = (all[0] && all[0].runner_mode) || 'first_pass_domain_diversity';\nfunction moscowIsoNow(){var m=new Date(Date.now()+10800000);return m.toISOString().replace('Z','+03:00');}\nconst now = moscowIsoNow();\n\nconst selected_urls = selected.map(r => r.candidate_url);\nconst selected_domains = Array.from(new Set(selected.map(r => r.domain).filter(Boolean)));\nconst domain_selected_counts = {};\nfor (const r of selected){ if (r.domain){ domain_selected_counts[r.domain] = (domain_selected_counts[r.domain]||0) + 1; } }\n\nconst handoff_lines = selected_urls.map(function(u){ return \"  '\" + u + \"',\"; }).join('\\n');\nconst set_url_list_block = 'const rawUrls = [\\n' + handoff_lines + '\\n];';\n\nconst domain_diversity = (runner_mode === 'deep_domain_analysis')\n  ? 'deep_domain_analysis: multiple URLs per domain allowed intentionally (cap 3/domain/run); extras skipped as domain_deep_limit. url_registry semantics unchanged.'\n  : 'first_pass_domain_diversity (default): max 1 URL per domain per run; second+ URL from a selected domain skipped as duplicate_domain_in_run. url_registry semantics unchanged.';\n\nreturn [{ json: {\n  workflow: '06 - Approved Candidates Runner',\n  run_id: run_id,\n  generated_at: now,\n  total_candidates_read: all.length,\n  selected_count: selected.length,\n  skipped_count: skipped.length,\n  registry_recheck_duplicate: registry_recheck_duplicate,\n  confirm_processed_count: confirm_processed_count,\n  max_per_run: 5,\n  runner_mode: runner_mode,\n  processing_mode: 'manual_handoff_to_workflow_04',\n  registry_recheck: 'enabled: url_registry re-read at runtime, candidate_url re-normalized; editable dedup_status/registry_status are advisory only',\n  domain_diversity: domain_diversity,\n  domain_selected_counts: domain_selected_counts,\n  selected_domains: selected_domains,\n  selected_urls: selected_urls,\n  selected: selected.map(function(r){ return { batch_index:r.batch_index, candidate_id:r.candidate_id, candidate_url:r.candidate_url, normalized_source_url:r.normalized_source_url, domain:r.domain, is_root:r.is_root, candidate_type:r.candidate_type, confidence_score:r.confidence_score, rank:r.rank, service_hint:r.service_hint, discovery_request_id:r.discovery_request_id, warning:r.warning }; }),\n  skipped: skipped.map(function(r){ return { candidate_id:r.candidate_id, candidate_url:r.candidate_url, domain:r.domain, candidate_type:r.candidate_type, reason_category:r._skip_category, reason:r._skip_reason }; }),\n  workflow_04_set_url_list_block: set_url_list_block,\n  operator_note: (selected.length === 0)\n    ? 'No eligible candidates after runtime registry recheck. Approve at least one direct_competitor candidate whose normalized URL is NOT already in url_registry, then re-run.'\n    : 'Copy the URLs above into Workflow 04 -> Set URL List (max 5), run Workflow 04. Marking processed is AUTOMATIC and SAFE: the enabled \"Mark Candidates Processed (confirmed in url_registry)\" node sets approval_status=processed ONLY for approved candidates whose normalized_source_url now appears in url_registry (i.e. WF04 actually wrote them). It never marks skipped/failed candidates and never marks freshly-selected handoffs (they stay approved until confirmed). Idempotent: re-running WF06 after WF04 confirms + marks; already-processed rows are skipped. Flow: approve -> WF06 (handoff) -> run WF04 -> re-run WF06 (auto-confirm + mark). runner_mode=' + runner_mode + '. Items carrying a warning need review before Workflow 04.'\n}}];"
      }
    }
  ],
  "connections": {
    "Manual Start": {
      "main": [
        [
          {
            "node": "Set Runner Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read url_candidates": {
      "main": [
        [
          {
            "node": "Read url_registry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read url_registry": {
      "main": [
        [
          {
            "node": "Select, Prioritize & Annotate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select, Prioritize & Annotate": {
      "main": [
        [
          {
            "node": "IF Confirmed Processed?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Build Execution Summary & Handoff",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Runner Config": {
      "main": [
        [
          {
            "node": "Read url_candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Confirmed Processed?": {
      "main": [
        [
          {
            "node": "Mark Candidates Processed (confirmed in url_registry)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a6000000-approved-candidates-runner-v004-runner-modes-20260607"
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

06 - Approved Candidates Runner. Uses googleSheets. Event-driven trigger; 10 nodes.

Source: https://github.com/CodeVinci8/vinci-ai-pilot/blob/main/n8n/workflows/06_approved_candidates_runner.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

TrackCollect_deeper. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 80 nodes.

Google Sheets, HTTP Request, @N Octo N/N8N Nodes Json Database +1
Data & Sheets

This template is ideal for solo store owners, eCommerce marketers, automation beginners, or anyone using Shopify and Gmail who wants to recover lost revenue without coding.

HTTP Request, Gmail, Twilio +3
Data & Sheets

PCN. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 60 nodes.

Google Sheets, HTTP Request, @N Octo N/N8N Nodes Json Database +3
Data & Sheets

Product Ad Machine. Uses googleDrive, googleSheets, httpRequest, @ffmpeg-micro/n8n-nodes-ffmpeg-micro. Event-driven trigger; 60 nodes.

Google Drive, Google Sheets, HTTP Request +2
Data & Sheets

Basic AI SEO KW Research n8n DataForSEO Gumroad 030125. Uses googleSheets, googleDrive, httpRequest. Event-driven trigger; 56 nodes.

Google Sheets, Google Drive, HTTP Request