ICP criteria in → Prospects found → Enriched → ICP scored → Deduped → HubSpot pushed → Opener written
Eliminates manual B2B prospecting. Built for German Mittelstand outbound workflows.
// STEP 1: Validate ICP criteria // n8n: Code node as first node after Webhook/Schedule trigger const validateICP = (p) => { if (!p.industry) throw new Error("Industry is required"); if (!p.product) throw new Error("Product description is required"); return { industry: p.industry, region: p.region || "germany", size: p.size || "sme", title: p.title || "procurement", product: p.product, leadCount: parseInt(p.leadCount) || 10, threshold: parseInt(p.threshold) || 70, runId: Math.random().toString(36).slice(2,10) }; };
// STEP 2: Find matching companies // Production: Apollo.io API for verified real contacts // Fallback: OpenAI generates realistic prospect data const OPENAI_API_KEY = "ADD_YOUR_OPENAI_KEY_HERE"; // REPLACE const APOLLO_API_KEY = "ADD_YOUR_APOLLO_KEY_HERE"; // REPLACE (optional) const searchCompanies = async (icp) => { // Try Apollo first (real verified contacts) if (APOLLO_API_KEY && !APOLLO_API_KEY.includes("ADD_YOUR")) { const r = await fetch("https://api.apollo.io/v1/mixed_people/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: APOLLO_API_KEY, q_organization_name: icp.industry, person_titles: [icp.title], organization_locations: [icp.region], page: 1, per_page: icp.leadCount }) }); const d = await r.json(); return d.people.map(p => ({ name: p.name, title: p.title, company: p.organization_name, email: p.email, linkedin: p.linkedin_url, location: p.city+", "+p.country, employees: p.organization?.num_employees, revenue: p.organization?.annual_revenue_printed })); } // Fallback: OpenAI generates realistic prospect data const r = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-4o-mini", response_format: {type:"json_object"}, messages: [{role:"system",content:"B2B researcher. Return ONLY valid JSON."}, {role:"user",content:`Generate ${icp.leadCount} B2B prospects. Industry:${icp.industry} Region:${icp.region} Size:${icp.size} Title:${icp.title} Product:${icp.product} Return: {"prospects":[{"name":"","title":"","company":"","email":"","linkedin":"","location":"","employees":0,"revenue":"","recentActivity":"","painPoints":[]}]}`}], temperature:0.6 }) }); const d = await r.json(); return JSON.parse(d.choices[0].message.content).prospects; };
// STEPS 3+4: Enrich data and score each lead 0-100 against ICP // Scores on: industry fit (30), size fit (20), title fit (25), geo fit (15), timing (10) const scoreLeads = async (prospects, icp) => { const r = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: {"Authorization":`Bearer ${OPENAI_API_KEY}`,"Content-Type":"application/json"}, body: JSON.stringify({ model:"gpt-4o-mini", response_format:{type:"json_object"}, temperature:0.2, messages:[ {role:"system",content:"B2B sales strategist. Score leads 0-100 against ICP. Return ONLY valid JSON."}, {role:"user",content:`Product:${icp.product} ICP:${icp.industry}|${icp.region}|${icp.size}|${icp.title} Prospects:${JSON.stringify(prospects.map((p,i)=>({index:i,title:p.title,company:p.company,location:p.location,employees:p.employees,recentActivity:p.recentActivity})))} Score each on: industryFit(30)+sizeFit(20)+titleFit(25)+geoFit(15)+timing(10)=100 Return: {"scored":[{"index":0,"icpScore":85,"scoreBreakdown":{"industryFit":28,"sizeFit":18,"titleFit":22,"geoFit":12,"timing":5},"scoreReason":"one sentence","priority":"hot|warm|cold"}]}`} ] }) }); const d = await r.json(); const scores = JSON.parse(d.choices[0].message.content).scored; return prospects.map((p,i) => ({...p, ...scores.find(s=>s.index===i)||{}})); };
// STEP 5: Check HubSpot for existing contacts by email // Skips if HubSpot not configured const HUBSPOT_TOKEN = "ADD_YOUR_HUBSPOT_TOKEN_HERE"; // REPLACE const deduplicate = async (leads) => { if (!HUBSPOT_TOKEN || HUBSPOT_TOKEN.includes("ADD_YOUR")) return leads.map(l => ({...l, existsInCRM:false})); return Promise.all(leads.map(async lead => { try { const r = await fetch("https://api.hubapi.com/crm/v3/objects/contacts/search", { method: "POST", headers: {"Authorization":`Bearer ${HUBSPOT_TOKEN}`,"Content-Type":"application/json"}, body: JSON.stringify({ filterGroups:[{filters:[{propertyName:"email",operator:"EQ",value:lead.email}]}] }) }); const d = await r.json(); return {...lead, existsInCRM: d.total > 0}; } catch { return {...lead, existsInCRM:false}; } })); };
// STEP 6: Create Contact + Company in HubSpot // Creates company record first, then links contact to it const pushToHubSpot = async (lead) => { if (!HUBSPOT_TOKEN || HUBSPOT_TOKEN.includes("ADD_YOUR") || lead.existsInCRM) return {skipped:true}; const h = {"Authorization":`Bearer ${HUBSPOT_TOKEN}`,"Content-Type":"application/json"}; const co = await (await fetch("https://api.hubapi.com/crm/v3/objects/companies",{ method:"POST", headers:h, body:JSON.stringify({properties:{ name:lead.company, city:lead.location.split(",")[0], numberofemployees:lead.employees?.toString(), hs_lead_status:"NEW" }}) })).json(); const [fn,...ln] = lead.name.split(" "); const ct = await (await fetch("https://api.hubapi.com/crm/v3/objects/contacts",{ method:"POST", headers:h, body:JSON.stringify({ properties:{ firstname:fn, lastname:ln.join(" "), email:lead.email, jobtitle:lead.title, linkedinbio:lead.linkedin, hs_lead_status:"NEW", // Custom properties (create in HubSpot Settings first): icp_score:lead.icpScore?.toString(), icp_priority:lead.priority, ai_opener:lead.opener }, associations:[{to:{id:co.id},types:[{associationCategory:"HUBSPOT_DEFINED",associationTypeId:1}]}] }) })).json(); return {contactId:ct.id, companyId:co.id}; };
// STEP 7: Write personalised first-line opener per lead // German B2B tone: direct, formal Sie-form, no hype, specific reference const writeOpener = async (lead, product, region) => { const r = await fetch("https://api.openai.com/v1/chat/completions", { method:"POST", headers:{"Authorization":`Bearer ${OPENAI_API_KEY}`,"Content-Type":"application/json"}, body:JSON.stringify({ model:"gpt-4o-mini", max_tokens:80, temperature:0.8, messages:[ {role:"system",content:`B2B sales writer for ${region}. Write ONE personalised opening sentence (max 25 words). ${region==="germany"?"Write in formal German (Sie-form).":"Write in professional English."} Reference something specific. No product pitch yet.`}, {role:"user",content:`${lead.name} | ${lead.title} @ ${lead.company} Location:${lead.location} | Activity:${lead.recentActivity} Pain points:${(lead.painPoints||[]).join(",")} My product:${product} Write opener:`} ] }) }); const d = await r.json(); return d.choices[0].message.content.trim(); };
// MASTER RUNNER: chains all 7 steps // n8n: each function = one node connected in sequence const runLeadAgent = async (rawParams) => { try { const icp = validateICP(rawParams); const prospects = await searchCompanies(icp); console.log(`[02] ${prospects.length} prospects found`); const scored = await scoreLeads(prospects, icp); const qualified = scored.filter(l => (l.icpScore||0) >= icp.threshold); console.log(`[04] ${qualified.length} qualified above ${icp.threshold}`); const deduped = await deduplicate(qualified); const newLeads = deduped.filter(l => !l.existsInCRM); console.log(`[05] ${newLeads.length} new leads after dedup`); await Promise.all(newLeads.map(pushToHubSpot)); console.log("[06] HubSpot push done"); const final = await Promise.all( newLeads.map(async l => ({...l, opener: await writeOpener(l, icp.product, icp.region)})) ); console.log(`[07] Done. ${final.length} leads ready.`); return { success:true, leads:final }; } catch(e) { return { success:false, error:e.message }; } }; // Run it: runLeadAgent({ industry:"chemical", region:"germany", size:"sme", title:"procurement", leadCount:10, threshold:70, product:"Epoxy resin solutions for industrial manufacturers" }).then(r => console.log(r.leads?.length+" leads ready"));