Lead Enrichment & Outreach LangGraph
What it does
A web-enrichment agent and a CRM-lookup agent run in parallel on a new lead. A scorer combines what they find into a fit score against the ideal-customer profile and marks the lead qualified or not. Qualified leads flow to a drafter that writes a specific first-touch email grounded in the enrichment, and the workflow pauses for human approval before sending. Point the CRM server at your own instance and adjust the ICP.
The cast · 4 agents
Combine the web and CRM findings into a fit score against the ideal-customer profile. Return an object with a 'qualified' boolean, the score, and the top reasons. Do not qualify a lead that is already an active opportunity owned by someone else.
Write a short, specific first-touch email grounded in the enrichment — reference something real about them, not a template. One clear ask. Save it as a draft; do not send.
Look up the lead in the CRM. Report whether they or their company already exist, prior touches, deal stage, and owner. If new, say so.
Research the lead's company and role from the open web: what the company does, size signals, recent news, and the person's likely priorities. Report facts with sources. Do not guess.
Flow
Interface
Depends on · 4 MCP servers
HubSpot MCPio.github.Servosity/hubspot-mcpcrmBVerified
io.github.brave/brave-search-mcpnot in the registry yetsearch
Multi-account MCP for Gmail, Calendar, Drive, Docs, and Sheets — 61 tools, tokenio.github.adelaidasofia/google-workspace-mcpmailAVerified
io.github.slack/slack-mcpnot in the registry yetchat# Generated by @socketcat/compiler for target: langgraph
# blueprint: com.socketcat/lead-enrichment v1.0.0 schema: socketcat.dev/blueprint/v0
# This code is yours. Edit it freely. The socketcat_runtime helper is optional and can be vendored.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
import socketcat_runtime as _rt
class State(TypedDict, total=False):
lead: object
outcome: object
webData: object
crmData: object
score: object
email: object
def node_Fork_0(state):
return {}
def node_Join_1(state):
return {}
def node_AgentInvoke_2(state):
return _rt.run_agent("webEnricher", state, {"lead":"lead"}, "webData")
def node_AgentInvoke_3(state):
return _rt.run_agent("crmLookup", state, {"lead":"lead"}, "crmData")
def node_AgentInvoke_4(state):
return _rt.run_agent("scorer", state, {"crmData":"crmData","webData":"webData"}, "score")
def node_Branch_5(state):
return {}
def node_Join_6(state):
return {}
def node_AgentInvoke_7(state):
return _rt.run_agent("drafter", state, {"score":"score","webData":"webData"}, "email")
def node_Ask_8(state):
return _rt.run_ask("Review the drafted outreach email. Approve to queue it for send, or edit first.", state, "outcome")
def node_AgentInvoke_9(state):
return _rt.run_agent("scorer", state, {"score":"score"}, "outcome")
def route_Branch_5(state):
if _rt.cond("score.qualified == true", state): return "AgentInvoke_7"
return "AgentInvoke_9"
def build():
b = StateGraph(State)
b.add_node("Fork_0", node_Fork_0)
b.add_node("Join_1", node_Join_1)
b.add_node("AgentInvoke_2", node_AgentInvoke_2)
b.add_node("AgentInvoke_3", node_AgentInvoke_3)
b.add_node("AgentInvoke_4", node_AgentInvoke_4)
b.add_node("Branch_5", node_Branch_5)
b.add_node("Join_6", node_Join_6)
b.add_node("AgentInvoke_7", node_AgentInvoke_7)
b.add_node("Ask_8", node_Ask_8)
b.add_node("AgentInvoke_9", node_AgentInvoke_9)
b.add_edge(START, "Fork_0")
b.add_edge("Fork_0", "AgentInvoke_2")
b.add_edge("Fork_0", "AgentInvoke_3")
b.add_edge("Join_1", "AgentInvoke_4")
b.add_edge("AgentInvoke_2", "Join_1")
b.add_edge("AgentInvoke_3", "Join_1")
b.add_edge("AgentInvoke_4", "Branch_5")
b.add_conditional_edges("Branch_5", route_Branch_5, {"AgentInvoke_7": "AgentInvoke_7", "AgentInvoke_9": "AgentInvoke_9"})
b.add_edge("Join_6", END)
b.add_edge("AgentInvoke_7", "Ask_8")
b.add_edge("Ask_8", "Join_6")
b.add_edge("AgentInvoke_9", "Join_6")
return b.compile()
INPUTS = ["lead"]
OUTPUTS = ["outcome"]
if __name__ == "__main__":
_rt.main(build, INPUTS, OUTPUTS)
▸blueprint.json (the portable format)
{
"id": "com.socketcat/lead-enrichment",
"flow": {
"type": "sequence",
"blocks": [
{
"type": "parallel",
"blocks": [
{
"in": {
"lead": "lead"
},
"out": "webData",
"use": "webEnricher",
"type": "agent"
},
{
"in": {
"lead": "lead"
},
"out": "crmData",
"use": "crmLookup",
"type": "agent"
}
]
},
{
"in": {
"crmData": "crmData",
"webData": "webData"
},
"out": "score",
"use": "scorer",
"type": "agent"
},
{
"else": [
{
"in": {
"score": "score"
},
"out": "outcome",
"use": "scorer",
"type": "agent"
}
],
"type": "branch",
"cases": [
{
"when": "score.qualified == true",
"blocks": [
{
"in": {
"score": "score",
"webData": "webData"
},
"out": "email",
"use": "drafter",
"type": "agent"
},
{
"out": "outcome",
"type": "ask",
"prompt": "Review the drafted outreach email. Approve to queue it for send, or edit first."
}
]
}
]
}
]
},
"tags": [
"sales",
"lead-gen",
"orchestrator-workers",
"human-in-the-loop"
],
"title": "Lead Enrichment & Outreach",
"agents": {
"scorer": {
"model": {
"hint": "reasoning"
},
"title": "Scorer",
"output": {
"type": "object",
"required": [
"qualified"
],
"properties": {
"score": {
"type": "number"
},
"reasons": {
"type": "string"
},
"qualified": {
"type": "boolean"
}
}
},
"instructions": "Combine the web and CRM findings into a fit score against the ideal-customer profile. Return an object with a 'qualified' boolean, the score, and the top reasons. Do not qualify a lead that is already an active opportunity owned by someone else."
},
"drafter": {
"model": {
"hint": "reasoning"
},
"title": "Drafter",
"tools": [
"mail.draft_email"
],
"instructions": "Write a short, specific first-touch email grounded in the enrichment — reference something real about them, not a template. One clear ask. Save it as a draft; do not send."
},
"crmLookup": {
"model": {
"hint": "fast"
},
"title": "CRM Lookup",
"tools": [
"crm.get_contact",
"crm.get_company"
],
"instructions": "Look up the lead in the CRM. Report whether they or their company already exist, prior touches, deal stage, and owner. If new, say so."
},
"webEnricher": {
"model": {
"hint": "fast"
},
"title": "Web Enricher",
"tools": [
"search.web"
],
"instructions": "Research the lead's company and role from the open web: what the company does, size signals, recent news, and the person's likely priorities. Report facts with sources. Do not guess."
}
},
"estate": {
"email": {
"type": "object",
"description": "The drafter's tailored first-touch email."
},
"score": {
"type": "object",
"description": "The scorer's fit read, including a 'qualified' flag."
},
"crmData": {
"type": "object",
"description": "The CRM lookup's existing record and prior touches."
},
"webData": {
"type": "object",
"description": "The web enricher's firmographic and role findings."
}
},
"$schema": "socketcat.dev/blueprint/v0",
"authors": [
{
"url": "https://socketcat.com",
"name": "SocketCat"
}
],
"license": "MIT",
"servers": [
{
"ref": "io.github.Servosity/hubspot-mcp",
"alias": "crm"
},
{
"ref": "io.github.brave/brave-search-mcp",
"alias": "search"
},
{
"ref": "io.github.adelaidasofia/google-workspace-mcp",
"alias": "mail"
},
{
"ref": "io.github.slack/slack-mcp",
"alias": "chat"
}
],
"summary": "Enriches a new lead from the web and CRM in parallel, scores it against your ICP, and drafts a tailored first email for human approval.",
"targets": [
"langgraph",
"*"
],
"version": "1.0.0",
"interface": {
"inputs": {
"lead": {
"type": "object",
"description": "The inbound lead: at least a name, email, and company."
}
},
"outputs": {
"outcome": {
"type": "string",
"description": "Whether the lead qualified and, if so, the approved email that was queued to send."
}
}
},
"extensions": {
"dev.langgraph": {
"checkpointer": "memory"
}
},
"description": "A web-enrichment agent and a CRM-lookup agent run in parallel on a new lead. A scorer combines what they find into a fit score against the ideal-customer profile and marks the lead qualified or not. Qualified leads flow to a drafter that writes a specific first-touch email grounded in the enrichment, and the workflow pauses for human approval before sending. Point the CRM server at your own instance and adjust the ICP."
}