CogniLead — Inbound Lead Qualification & CRM Enrichment Agent
Qualifies inbound leads and enriches CRM records automatically, without manual review

The Problem
Every inbound lead needs the same repetitive work before it's usable: figuring out whether the person has real buying authority, verifying the company they claim to represent actually exists and matches what they said, and getting all of that written cleanly into a CRM. Done manually, this is slow and inconsistent, and mistakes (like trusting an unverified company match) can quietly pollute CRM data in ways that are hard to catch later.
The Solution
CogniLead is a LangGraph agent that takes a raw inbound lead and runs it through extraction and scoring nodes that infer role and authority even when it's only implied, not stated outright. A CompanyEnrichmentNode then verifies the company using Tavily search combined with LLM synthesis. From there, a fully deterministic (non-LLM) human-review gate routes each lead to auto-accept, auto-reject, or human review based on score and enrichment confidence thresholds, so the decision of when a human needs to step in isn't itself something the model can talk its way around. Accepted leads go through a CRM write-back node that deduplicates both contacts and companies by search, creates or updates records accordingly, links them through HubSpot's associations API, and writes a note with the score and reasoning attached. The write order is deliberately sequenced, company, then contact, then association, then note, and a dedicated retry node checks exactly which step already succeeded before retrying, so a failed write resumes from the next incomplete step instead of duplicating work.
Tech Stack
A Real Bug, In Detail
During enrichment testing, the agent once found a company with the same name as the one the lead actually worked for, but it was an entirely unrelated business, and scored it as a verified match. Nothing in the original design distinguished 'a company with this name exists' from 'this is actually the right company.' The fix was adding two new fields, enrichment_status and name_match_confidence, so a mismatch gets flagged explicitly in the CRM note rather than silently trusted as verified. This mattered because a wrong company match wouldn't just be a bad data point, it would look correct in HubSpot and could mislead a real sales rep working the lead later.
Decisions and Tradeoffs
When a lead comes in with no stated company name, CogniLead skips company creation entirely rather than fabricating a placeholder company record, the contact is still created, with a note explicitly flagging that no company was stated in the original submission. This was chosen over two other options, requiring a company name at ingestion (which would reject real leads) or inventing a placeholder name (which would quietly pollute the CRM with fake companies). On the reliability side, the retry logic has a hard ceiling with backoff, after the maximum attempts it marks a write as failed_permanent instead of looping forever against a write that's genuinely broken, which matters because an infinite retry against a bad write is its own kind of production incident.
Lessons Learned
A resume bug taught a lesson about testing assumptions, not just code paths. The resume logic was calling graph.invoke fresh on every call instead of resuming through the existing thread_id, which generated a brand new thread each time and made a perfectly working workflow look like it was restarting from the top on every single resume. It looked like a state-loss bug at first glance, but the actual state was fine, the resume call itself was just never using it. The fix was small, but finding it required specifically testing the resume path in isolation rather than assuming that because the initial run worked, resume would work the same way. Since then, every checkpoint-dependent feature in later projects gets a dedicated resume test, not just a pass on the first-run happy path.