{
  "name": "02 — Enrichment (every 10 min)",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/10 * * * *"
            }
          ]
        }
      },
      "id": "wf02-0001",
      "name": "Every 10 Min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM leads.leads WHERE state = 'new' ORDER BY created_at ASC LIMIT 5",
        "additionalFields": {}
      },
      "id": "wf02-0002",
      "name": "Fetch 5 New Leads",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "wf02-0003",
      "name": "Split Leads",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Token-bucket throttle: max 25 RPM against Groq free tier\n// Reads/writes groq_rpm_window in system_state as a JSON array of ISO timestamps\nconst now = Date.now();\nconst windowMs = 60000;\n\n// We'll check after the Postgres read below — this node just passes through\n// The actual check is done in 'Check Throttle' after reading system_state\nreturn [{ json: { ...$json, _ts: now } }];"
      },
      "id": "wf02-0004",
      "name": "Attach Timestamp",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT value FROM leads.system_state WHERE key = 'groq_rpm_window'",
        "additionalFields": {}
      },
      "id": "wf02-0005",
      "name": "Read RPM Window",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const limit = parseInt($env.GROQ_RPM_LIMIT || '25');\nconst now = Date.now();\nconst windowMs = 60000;\nconst raw = $json.value || '[]';\nlet timestamps = JSON.parse(raw);\ntimestamps = timestamps.filter(t => now - t < windowMs);\n\nif (timestamps.length >= limit) {\n  // Throttled — skip this lead\n  return [{ json: { _throttled: true, timestamps } }];\n}\n\ntimestamps.push(now);\nreturn [{ json: { _throttled: false, timestamps, lead: $('Split Leads').item.json } }];"
      },
      "id": "wf02-0006",
      "name": "Check Throttle",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "throttle-check",
              "leftValue": "={{ $json._throttled }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equal"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "wf02-0007",
      "name": "Is Throttled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1560,
        300
      ]
    },
    {
      "parameters": {},
      "id": "wf02-0008",
      "name": "Skip (rate limited)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1780,
        180
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "http://playwright-worker:8000/scrape-website",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "domain",
              "value": "={{ $json.lead.domain }}"
            }
          ]
        },
        "options": {
          "timeout": 90000
        }
      },
      "id": "wf02-0009",
      "name": "Scrape Website",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1780,
        420
      ]
    },
    {
      "parameters": {
        "jsCode": "const scrape = $json;\nconst lead = $('Check Throttle').item.json.lead;\n\n// English signals\nconst hreflang_en = scrape.hreflang_en || false;\nconst html_lang_en = scrape.html_lang_en || false;\nconst lang_switcher_en = scrape.lang_switcher_en || false;\nconst texts = Object.values(scrape.raw_text_by_path || {});\nconst combined = texts.join('\\n');\n\n// Truncate combined text for Groq prompt\nconst truncated = combined.substring(0, 6000);\n\nreturn [{ json: {\n  lead,\n  scrape,\n  hreflang_en,\n  html_lang_en,\n  lang_switcher_en,\n  truncated_text: truncated,\n  unique_internal_links: scrape.unique_internal_links || 0,\n  team_entries: scrape.team_entries || 0,\n  whois_reg_date: scrape.whois_reg_date || null,\n} }];"
      },
      "id": "wf02-0010",
      "name": "Extract Signals",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2000,
        420
      ]
    },
    {
      "parameters": {
        "jsCode": "const d = $json;\nconst systemPrompt = `You are an expert business analyst. Analyze this company website content and return ONLY a JSON object matching this exact schema. No markdown, no explanation — raw JSON only.\n\nSchema:\n{\n  \"services_offered\": \"string\",\n  \"primary_language\": \"string\",\n  \"automation_pain_signals\": [\"string\"],\n  \"contacts_found\": [{\"full_name\": \"string\", \"role_title\": \"string\", \"email_if_visible\": \"string or null\", \"source_page\": \"string\"}],\n  \"fit_score\": 1,\n  \"why_now_hook\": \"string\",\n  \"english_friendly_verdict\": \"yes\",\n  \"english_friendly_reason\": \"string\",\n  \"visible_team_size\": null,\n  \"operational_complexity_signals\": [\"string\"]\n}\n\nfit_score is 1-5 integer. english_friendly_verdict must be exactly \\\"yes\\\", \\\"no\\\", or \\\"ambiguous\\\".`;\n\nconst userMessage = `Company: ${d.lead.company_name}\\nDomain: ${d.lead.domain}\\nCountry: ${d.lead.country}\\n\\nWebsite content:\\n${d.truncated_text}`;\n\nreturn [{ json: { ...d, groq_system: systemPrompt, groq_user: userMessage } }];"
      },
      "id": "wf02-0011",
      "name": "Build Groq Prompt",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2220,
        420
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.groq.com/openai/v1/chat/completions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  model: 'llama-3.1-8b-instant',\n  messages: [\n    { role: 'system', content: $json.groq_system },\n    { role: 'user', content: $json.groq_user }\n  ],\n  temperature: 0.1,\n  max_tokens: 1024,\n  response_format: { type: 'json_object' }\n}) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "wf02-0012",
      "name": "Call Groq 8B",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2440,
        420
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $json;\nconst prev = $('Build Groq Prompt').item.json;\n\nlet parsed = {};\ntry {\n  const content = response.choices[0].message.content;\n  parsed = JSON.parse(content);\n} catch(e) {\n  parsed = { fit_score: 1, english_friendly_verdict: 'ambiguous', contacts_found: [] };\n}\n\nreturn [{ json: { ...prev, groq_result: parsed } }];"
      },
      "id": "wf02-0013",
      "name": "Parse Groq Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2660,
        420
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE leads.leads SET\n  state = 'enriched',\n  score_int = $1,\n  score_reason = $2,\n  english_friendly_verdict = $3,\n  hreflang_en = $4,\n  html_lang_en = $5,\n  lang_switcher_en = $6,\n  visible_team_size = $7,\n  unique_internal_links = $8,\n  operational_complexity_signals = $9::jsonb,\n  whois_reg_date = $10::date,\n  raw_scrape_data = $11::jsonb,\n  updated_at = NOW()\nWHERE id = $12",
        "additionalFields": {
          "queryParams": "={{ JSON.stringify([\n  $json.groq_result.fit_score || 1,\n  $json.groq_result.why_now_hook || '',\n  $json.groq_result.english_friendly_verdict || 'ambiguous',\n  $json.hreflang_en,\n  $json.html_lang_en,\n  $json.lang_switcher_en,\n  $json.groq_result.visible_team_size || null,\n  $json.unique_internal_links,\n  JSON.stringify($json.groq_result.operational_complexity_signals || []),\n  $json.whois_reg_date || null,\n  JSON.stringify($json.scrape),\n  $json.lead.id\n]) }}"
        }
      },
      "id": "wf02-0014",
      "name": "Update Lead Row",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2880,
        420
      ]
    },
    {
      "parameters": {
        "jsCode": "const contacts = $('Parse Groq Response').item.json.groq_result.contacts_found || [];\nconst lead_id = $('Parse Groq Response').item.json.lead.id;\n\nif (!contacts.length) return [{ json: { inserted: 0 } }];\n\nreturn contacts.map(c => ({ json: { lead_id, ...c } }));"
      },
      "id": "wf02-0015",
      "name": "Prepare Contacts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3100,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO leads.lead_contacts (lead_id, full_name, role_title, email, email_source, found_via, role_tier)\nVALUES ($1, $2, $3, $4, 'website', $5, 'other')\nON CONFLICT DO NOTHING",
        "additionalFields": {
          "queryParams": "={{ JSON.stringify([\n  $json.lead_id,\n  $json.full_name,\n  $json.role_title,\n  $json.email_if_visible || null,\n  $json.source_page || 'website'\n]) }}"
        }
      },
      "id": "wf02-0016",
      "name": "Insert Contacts",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        3320,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO leads.system_state (key, value) VALUES ('groq_rpm_window', $1)\nON CONFLICT (key) DO UPDATE SET value = $1",
        "additionalFields": {
          "queryParams": "={{ JSON.stringify([JSON.stringify($('Check Throttle').item.json.timestamps)]) }}"
        }
      },
      "id": "wf02-0017",
      "name": "Update RPM Counter",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        3540,
        420
      ]
    }
  ],
  "connections": {
    "Every 10 Min": {
      "main": [
        [
          {
            "node": "Fetch 5 New Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch 5 New Leads": {
      "main": [
        [
          {
            "node": "Split Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Leads": {
      "main": [
        [],
        [
          {
            "node": "Attach Timestamp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Timestamp": {
      "main": [
        [
          {
            "node": "Read RPM Window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read RPM Window": {
      "main": [
        [
          {
            "node": "Check Throttle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Throttle": {
      "main": [
        [
          {
            "node": "Is Throttled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Throttled?": {
      "main": [
        [
          {
            "node": "Skip (rate limited)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape Website",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Website": {
      "main": [
        [
          {
            "node": "Extract Signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Signals": {
      "main": [
        [
          {
            "node": "Build Groq Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Groq Prompt": {
      "main": [
        [
          {
            "node": "Call Groq 8B",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Groq 8B": {
      "main": [
        [
          {
            "node": "Parse Groq Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Groq Response": {
      "main": [
        [
          {
            "node": "Update Lead Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Lead Row": {
      "main": [
        [
          {
            "node": "Prepare Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Contacts": {
      "main": [
        [
          {
            "node": "Insert Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Contacts": {
      "main": [
        [
          {
            "node": "Update RPM Counter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update RPM Counter": {
      "main": [
        [
          {
            "node": "Split Leads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
