{
  "name": "Leadgen: enrich a CSV of Romanian firms (lookup_companies + match_companies + find_company_by_domain)",
  "nodes": [
    {
      "parameters": {},
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0001",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "fileSelector": "/files/leadgen-input.csv",
        "options": {}
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0002",
      "name": "Read the CSV file",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        220,
        300
      ],
      "notes": "Your list: one firm per row, first row = column names. Recognised columns (any case, RO or EN): cui / cif / cod fiscal; name / company / denumire / firma / client / cont; county / judet; website / domain / site / email. To read a Google Sheet instead, replace this node and the next one with a Google Sheets 'Get rows' node."
    },
    {
      "parameters": {
        "operation": "csv",
        "options": {
          "headerRow": true
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0003",
      "name": "Rows from the CSV",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Rows WITH a CUI -> POST /lookup_companies, up to 100 CUIs per request.\n// Each CUI counts as one lookup on your key (free key: 25 a day). A batch larger\n// than what is left today answers the CUIs that fit; the rest come back\n// not_processed (not counted) and go to leadgen-pending.csv for the next run.\nconst pick = (row, names) => {\n  for (const [k, v] of Object.entries(row)) {\n    const h = k.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().trim();\n    if (names.some((n) => h === n || h.startsWith(n + ' '))) return String(v ?? '').trim();\n  }\n  return '';\n};\nconst CUI = ['cui', 'cif', 'cod fiscal', 'cod_fiscal', 'tax id', 'vat number'];\nconst cuis = [];\nfor (const item of $input.all()) {\n  const cui = pick(item.json, CUI);\n  if (cui) cuis.push(cui);\n}\nconst out = [];\nfor (let i = 0; i < cuis.length; i += 100) {\n  out.push({ json: { cuis: cuis.slice(i, i + 100), compact: true } });\n}\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0004",
      "name": "Batches of CUIs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        180
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Rows WITHOUT a CUI but with a name -> POST /match_companies (name, county,\n// website), up to 500 rows per request. Each row counts as one lookup on your key. `ref` is the\n// row number, echoed back so the answer joins the right row.\nconst pick = (row, names) => {\n  for (const [k, v] of Object.entries(row)) {\n    const h = k.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().trim();\n    if (names.some((n) => h === n || h.startsWith(n + ' '))) return String(v ?? '').trim();\n  }\n  return '';\n};\nconst CUI = ['cui', 'cif', 'cod fiscal', 'cod_fiscal', 'tax id', 'vat number'];\nconst NAME = ['name', 'company', 'company name', 'account', 'denumire', 'firma', 'client', 'cont', 'nume'];\nconst COUNTY = ['county', 'judet', 'judetul'];\nconst SITE = ['website', 'domain', 'site', 'web', 'url', 'email', 'e-mail'];\nconst rows = [];\n$input.all().forEach((item, i) => {\n  if (pick(item.json, CUI)) return;\n  const name = pick(item.json, NAME);\n  const domain = pick(item.json, SITE);\n  if (!name) return;   // a website alone goes to /find_company_by_domain (Websites)\n  rows.push({ name: name || null, county: pick(item.json, COUNTY) || null, domain: domain || null, ref: String(i) });\n});\nconst out = [];\nfor (let i = 0; i < rows.length; i += 500) {\n  out.push({ json: { rows: rows.slice(i, i + 500), compact: true } });\n}\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0005",
      "name": "Batches of names",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        420
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://leadgen.adrianhomelab.com/lookup_companies",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0006",
      "name": "Leadgen /lookup_companies",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        920,
        180
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "",
          "name": "Leadgen API key"
        }
      },
      "notes": "Credential: Header Auth, Name = X-API-Key, Value = your key (free at https://leadgen.adrianhomelab.com/start). A batch larger than what is left today answers the rows that fit (partial: true, processed, remaining); a refusal (nothing left today: 429) comes back as the rows' status instead of stopping the run."
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://leadgen.adrianhomelab.com/match_companies",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0007",
      "name": "Leadgen /match_companies",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        920,
        420
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "",
          "name": "Leadgen API key"
        }
      },
      "notes": "Same credential as /lookup_companies. A batch larger than what is left today answers the rows that fit; the rest come back not_processed."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// One output row per CUI asked: the firm, or found=false with the reason. A CUI\n// today's allowance did not cover is \"not_processed\" (not run, not counted): the\n// answer says partial: true and `remaining`, and a request refused because nothing\n// was left (429) covers every CUI it carried. Those rows go to leadgen-pending.csv.\nconst asked = typeof $ === 'function' ? $('Batches of CUIs').all() : [];\nconst out = [];\n$input.all().forEach((item, i) => {\n  const b = item.json || {};\n  let results = b.results;\n  if (!Array.isArray(results)) {\n    const status = b.error === 'daily_limit_reached' ? 'not_processed' : (b.error || 'error');\n    results = (((asked[i] || {}).json || {}).cuis || []).map((c) => ({ input: c, found: false, status }));\n  }\n  for (const r of results) {\n    const c = r.company || {};\n    const fin = c.latestFinancials || {};\n    const pending = r.status === 'not_processed';\n    out.push({ json: {\n      input: r.input, leadgen_status: r.found ? 'found' : (r.status || r.reason || r.error || 'not_found'),\n      cui: r.cui || (pending ? String(r.input ?? '') : ''), company: c.companyName || '', county: c.county || '', city: c.city || '',\n      legal_form: c.legalForm || '', risk: (c.risk || {}).level || '', is_lead: (c.risk || {}).isLead ?? '',\n      phone: c.phone || '', website: c.website || '', revenue: fin.revenue ?? '', employees: fin.employees ?? '',\n      financial_year: fin.year ?? '', confidence: r.found ? 1 : '', matched_on: r.found ? 'cui' : '',\n      attribution: b.attribution || '',\n    } });\n  }\n});\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0008",
      "name": "Firms by CUI",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        180
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// One output row per name asked: the matched firm with its confidence, or\n// UNMATCHED with up to 3 candidates (never a guess). A row flagged ambiguous or\n// countyMismatch is matched but worth a look. A row today's allowance did not\n// cover is \"not_processed\" (not run, not counted; a 429 covers its whole request)\n// and keeps the name, county and website you gave, for leadgen-pending.csv.\nconst asked = typeof $ === 'function' ? $('Batches of names').all() : [];\nconst out = [];\n$input.all().forEach((item, i) => {\n  const b = item.json || {};\n  let results = b.results;\n  if (!Array.isArray(results)) {\n    const status = b.error === 'daily_limit_reached' ? 'not_processed' : (b.error || 'error');\n    results = (((asked[i] || {}).json || {}).rows || []).map((row) => ({ status, input: row, ref: row.ref }));\n  }\n  for (const r of results) {\n    const m = r.match || {};\n    const c = r.company || {};\n    const inp = r.input || {};\n    const fin = c.latestFinancials || {};\n    const pending = r.status === 'not_processed';\n    const flags = [r.ambiguous ? 'ambiguous' : '', r.countyMismatch ? 'county_mismatch' : ''].filter(Boolean).join(' ');\n    const status = r.status === 'matched' ? ('matched' + (flags ? ' (' + flags + ')' : ''))\n      : (r.status === 'no_match' || r.status === 'error' ? 'UNMATCHED' : (r.status || 'UNMATCHED'));\n    out.push({ json: {\n      input: [inp.name, inp.county, inp.domain].filter(Boolean).join(' / '),\n      leadgen_status: status,\n      cui: m.cui || '', company: m.companyName || c.companyName || (pending ? inp.name || '' : ''),\n      county: c.county || (pending ? inp.county || '' : ''), city: c.city || '',\n      legal_form: c.legalForm || '', risk: (c.risk || {}).level || '', is_lead: (c.risk || {}).isLead ?? '',\n      phone: c.phone || '', website: c.website || (pending ? inp.domain || '' : ''), revenue: fin.revenue ?? '',\n      employees: fin.employees ?? '', financial_year: fin.year ?? '', confidence: m.confidence ?? '', matched_on: m.matchedOn || '',\n      candidates: (r.candidates || []).slice(0, 3).map((x) => `${x.companyName} (${x.cui}, ${x.county || ''})`).join('; '),\n      attribution: b.attribution || '',\n    } });\n  }\n});\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0009",
      "name": "Firms by name",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        420
      ]
    },
    {
      "parameters": {
        "jsCode": "// Rows with ONLY a website (no CUI, no name) -> GET /find_company_by_domain, one\n// request per row: the registry's website field first, else the site's own pages\n// are read for the CUI they show (emag.ro -> DANTE INTERNATIONAL SA). One lookup\n// per row. `ref` is the row number.\nconst pick = (row, names) => {\n  for (const [k, v] of Object.entries(row)) {\n    const h = k.normalize('NFD').replace(/[\\u0300-\\u036f]/g, '').toLowerCase().trim();\n    if (names.some((n) => h === n || h.startsWith(n + ' '))) return String(v ?? '').trim();\n  }\n  return '';\n};\nconst CUI = ['cui', 'cif', 'cod fiscal', 'cod_fiscal', 'tax id', 'vat number'];\nconst NAME = ['name', 'company', 'company name', 'account', 'denumire', 'firma', 'client', 'cont', 'nume'];\nconst SITE = ['website', 'domain', 'site', 'web', 'url', 'email', 'e-mail'];\nconst out = [];\n$input.all().forEach((item, i) => {\n  if (pick(item.json, CUI) || pick(item.json, NAME)) return;\n  let domain = pick(item.json, SITE);\n  if (domain.includes('@')) domain = domain.split('@').pop();   // an email's domain\n  if (domain) out.push({ json: { domain, ref: String(i) } });\n});\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0013",
      "name": "Websites",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        660
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://leadgen.adrianhomelab.com/find_company_by_domain",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "domain",
              "value": "={{ $json.domain }}"
            },
            {
              "name": "compact",
              "value": "true"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0014",
      "name": "Leadgen /find_company_by_domain",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        920,
        660
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "",
          "name": "Leadgen API key"
        }
      },
      "notes": "Same credential as /lookup_companies. One request per website; a refusal (bad domain, daily limit) comes back as the row's status instead of stopping the run."
    },
    {
      "parameters": {
        "jsCode": "// One output row per website asked: the firm behind it (matched_on says how:\n// registry_website or site_cui), or UNMATCHED with up to 3 name candidates from\n// the page title, or the refusal (invalid_domain; not_processed when nothing was\n// left today: the website goes to leadgen-pending.csv).\nconst asked = typeof $ === 'function' ? $('Websites').all() : [];\nconst out = [];\n$input.all().forEach((item, i) => {\n  const b = item.json || {};\n  const c = (b.results || [])[0] || {};\n  const m = b.match || {};\n  const fin = c.latestFinancials || {};\n  const domain = ((asked[i] || {}).json || {}).domain || b.domain || '';\n  const pending = b.error === 'daily_limit_reached';   // nothing left today: resend tomorrow\n  out.push({ json: {\n    input: domain,\n    leadgen_status: pending ? 'not_processed' : (b.error ? b.error : (b.found ? 'matched' : 'UNMATCHED')),\n    cui: m.cui || '', company: m.companyName || c.companyName || '', county: c.county || '', city: c.city || '',\n    legal_form: c.legalForm || '', risk: (c.risk || {}).level || '', is_lead: (c.risk || {}).isLead ?? '',\n    phone: c.phone || '', website: c.website || (pending ? domain : ''), revenue: fin.revenue ?? '', employees: fin.employees ?? '',\n    financial_year: fin.year ?? '', confidence: '', matched_on: b.matchedOn || '',\n    candidates: (b.candidates || []).slice(0, 3).map((x) => `${x.companyName} (${x.cui}, ${x.county || ''})`).join('; '),\n    attribution: b.attribution || '',\n  } });\n});\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0015",
      "name": "Firms by website",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        660
      ]
    },
    {
      "parameters": {
        "numberInputs": 3
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0010",
      "name": "All rows",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        1400,
        300
      ]
    },
    {
      "parameters": {
        "options": {
          "fileName": "leadgen-enriched.csv"
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0011",
      "name": "To CSV",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        1620,
        300
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "/files/leadgen-enriched.csv",
        "options": {}
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0012",
      "name": "Write the enriched CSV",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        1840,
        300
      ],
      "notes": "Or send it on: Google Sheets 'Append row', email, your CRM. Keep the attribution column (CC BY 4.0) when you redistribute the data."
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// The rows today's allowance did not cover (leadgen_status \"not_processed\": not\n// run, not counted), in column names \"Read the CSV file\" recognises. After the\n// reset (00:00 UTC, 03:00 Romanian time) run the workflow on leadgen-pending.csv.\n// Nothing pending: this node outputs nothing and no file is written.\nconst out = [];\nfor (const item of $input.all()) {\n  const r = item.json || {};\n  if (r.leadgen_status !== 'not_processed') continue;\n  out.push({ json: { cui: r.cui || '', name: r.cui ? '' : (r.company || ''), county: r.cui ? '' : (r.county || ''),\n    website: r.cui ? '' : (r.website || '') } });\n}\nreturn out;"
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0016",
      "name": "Rows for tomorrow",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        520
      ]
    },
    {
      "parameters": {
        "options": {
          "fileName": "leadgen-pending.csv"
        }
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0017",
      "name": "Pending to CSV",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        1840,
        520
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "/files/leadgen-pending.csv",
        "options": {}
      },
      "id": "6f1c1e2a-1b1a-4d0e-9a51-0c0a3b7e0018",
      "name": "Write the rows for tomorrow",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        2060,
        520
      ],
      "notes": "Written only when today's allowance ran out partway. After 00:00 UTC (03:00 Romanian time) point 'Read the CSV file' at /files/leadgen-pending.csv (or copy it over leadgen-input.csv) and run again; keep this run's enriched CSV first, the next run writes a new one."
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Read the CSV file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read the CSV file": {
      "main": [
        [
          {
            "node": "Rows from the CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rows from the CSV": {
      "main": [
        [
          {
            "node": "Batches of CUIs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Batches of names",
            "type": "main",
            "index": 0
          },
          {
            "node": "Websites",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batches of CUIs": {
      "main": [
        [
          {
            "node": "Leadgen /lookup_companies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batches of names": {
      "main": [
        [
          {
            "node": "Leadgen /match_companies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leadgen /lookup_companies": {
      "main": [
        [
          {
            "node": "Firms by CUI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leadgen /match_companies": {
      "main": [
        [
          {
            "node": "Firms by name",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firms by CUI": {
      "main": [
        [
          {
            "node": "All rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firms by name": {
      "main": [
        [
          {
            "node": "All rows",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "All rows": {
      "main": [
        [
          {
            "node": "To CSV",
            "type": "main",
            "index": 0
          },
          {
            "node": "Rows for tomorrow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "To CSV": {
      "main": [
        [
          {
            "node": "Write the enriched CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Websites": {
      "main": [
        [
          {
            "node": "Leadgen /find_company_by_domain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leadgen /find_company_by_domain": {
      "main": [
        [
          {
            "node": "Firms by website",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Firms by website": {
      "main": [
        [
          {
            "node": "All rows",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Rows for tomorrow": {
      "main": [
        [
          {
            "node": "Pending to CSV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pending to CSV": {
      "main": [
        [
          {
            "node": "Write the rows for tomorrow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
