LangChain Cold Email: Build Production Outreach Agents with FoxReach
Wire FoxReach into a LangChain agent two ways - the Python SDK for tight integration, or the hosted MCP server for zero-config tool discovery. Working code for an agent that researches a lead, personalizes the copy, and sends through FoxReach.
Usama Navid
Founder, FoxReach
What you'll build
A LangChain agent that takes a CSV of leads, researches each one, writes a personalized cold email, and ships the first campaign through FoxReach. Under 30 minutes from zero to first send. At the end of the guide you will have working code that:
- Registers 23 FoxReach tools with a LangChain agent via MCP
- Creates a draft campaign and a 3-step sequence with one prompt
- Imports leads from a list, calling a research tool for each
- Logs every tool call to LangSmith for observability
- Leaves sending under human control until you trust the drafts
Prerequisites
Python 3.10+
LangChain 0.3 requires at least 3.10. 3.11 or 3.12 recommended.
A FoxReach account
Free plan works. Sign up at foxreach.io, create a workspace, connect one email account for sending.
A FoxReach API key
Generate from Settings → API Keys in your dashboard. Prefix: fr_. Treat it like a password.
An LLM provider
OpenAI, Anthropic, or any LangChain-compatible chat model. The examples below use GPT-4o-mini for cost.
Two integration paths
You can talk to FoxReach from LangChain in two ways. Both are production-ready. Pick by how much typing you want to do and whether you care about auto-discovery of new tools.
Path 1: Python SDK
Install the FoxReach Python SDK, then register each method you need as a LangChain tool. Full type safety, autocomplete, and the smallest runtime footprint.
pip install foxreach langchain langchain-openaiimport os
from foxreach import FoxReach
from langchain_core.tools import tool
client = FoxReach(api_key=os.environ["FOXREACH_API_KEY"])
@tool
def create_campaign(name: str) -> dict:
"""Create a new draft cold email campaign in FoxReach.
The campaign starts in draft state. Call start_campaign to begin sending.
"""
campaign = client.campaigns.create(name=name, status="draft")
return {"id": campaign.id, "name": campaign.name, "status": campaign.status}
@tool
def add_sequence_step(
campaign_id: str,
subject: str,
body: str,
wait_days: int = 3,
) -> dict:
"""Add an email step to a campaign sequence."""
step = client.sequences.create(
campaign_id=campaign_id,
subject=subject,
body=body,
wait_days=wait_days,
)
return {"id": step.id, "subject": step.subject}
@tool
def import_leads(campaign_id: str, leads: list[dict]) -> dict:
"""Import leads into a campaign. Each lead needs at minimum an email field."""
result = client.leads.create_batch(campaign_id=campaign_id, leads=leads)
return {"imported": len(result.created), "skipped": len(result.skipped)}The tools now behave like any other LangChain tool. Pass them to create_tool_calling_agent, or bind them to a chat model with llm.bind_tools([...]).
Path 2: MCP adapter
Use the langchain-mcp-adapters package to auto-register every tool the FoxReach MCP server exposes. New tools FoxReach ships appear in your agent automatically - no code change on your side.
pip install langchain-mcp-adapters langchain langchain-openaiimport os
import asyncio
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
async def main():
client = MultiServerMCPClient({
"foxreach": {
"url": "https://api.foxreach.io/mcp",
"transport": "streamable_http",
"headers": {
"Authorization": f"Bearer {os.environ['FOXREACH_API_KEY']}",
},
},
})
tools = await client.get_tools() # returns all 23 FoxReach MCP tools
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You are an outbound sales agent. Use FoxReach tools to manage campaigns and leads."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
await executor.ainvoke({
"input": "Create a draft campaign called 'SaaS CTOs April' and add a 3-step sequence: intro, value prop, soft ask. Three days between each.",
})
if __name__ == "__main__":
asyncio.run(main())Which path to pick
If you are writing one agent, shipping fast, and want type safety inside the tool wrapper, go with the SDK. If you are running many agents, want every new FoxReach tool to appear automatically, or you want to test the same infrastructure across LangChain + Claude Desktop + Cursor, go with MCP. You can also mix - use MCP for the read tools and the SDK for a couple of write tools where you want extra validation.
Build a real agent
The interesting part is not calling FoxReach - it is the research and personalization steps that make outbound work. Here is a three-step agent that chains a web research tool with FoxReach.
Step 1: Research the lead
For each lead, the agent fetches their LinkedIn headline + recent posts + their company's last funding announcement. Use any tool provider - Tavily, Serper, Exa, or a custom scraper you trust. This tool stays outside FoxReach.
from langchain_core.tools import tool
from langchain_community.tools.tavily_search import TavilySearchResults
search = TavilySearchResults(max_results=3)
@tool
def research_lead(name: str, company: str) -> str:
"""Find recent public context about a person and their company."""
query = f"{name} {company} site:linkedin.com OR site:crunchbase.com"
results = search.invoke(query)
return "\n\n".join(r["content"] for r in results)Step 2: Personalize the copy
The agent asks the LLM to turn research output + a base template into a personalized email. Keep the tool interface boring - the agent passes research text in, gets subject + body back.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel
class PersonalizedEmail(BaseModel):
subject: str
body: str
llm = ChatOpenAI(model="gpt-4o-mini").with_structured_output(PersonalizedEmail)
prompt = ChatPromptTemplate.from_messages([
("system", "You write concise, specific cold emails. Open with one concrete observation from the research. Under 90 words."),
("human", "Lead: {name}, {title} at {company}.\nResearch:\n{research}\n\nTemplate:\n{template}"),
])
@tool
def personalize_email(
name: str, title: str, company: str, research: str, template: str,
) -> dict:
"""Rewrite a template into a personalized email for this lead."""
chain = prompt | llm
result = chain.invoke({"name": name, "title": title, "company": company, "research": research, "template": template})
return {"subject": result.subject, "body": result.body}Step 3: Send via FoxReach
Tie it together. The agent creates a campaign, imports the lead, adds the personalized email as a one-step sequence, and leaves the campaign in draft for human approval.
from foxreach import FoxReach
client = FoxReach(api_key=os.environ["FOXREACH_API_KEY"])
def ship_personalized_campaign(lead: dict, template: str):
research = research_lead.invoke({
"name": lead["name"], "company": lead["company"],
})
email = personalize_email.invoke({
"name": lead["name"], "title": lead["title"],
"company": lead["company"], "research": research, "template": template,
})
campaign = client.campaigns.create(
name=f"Personalized - {lead['company']}",
status="draft",
)
client.sequences.create(
campaign_id=campaign.id,
subject=email["subject"],
body=email["body"],
wait_days=0,
)
client.leads.create(
campaign_id=campaign.id,
email=lead["email"],
first_name=lead["name"],
company=lead["company"],
)
return {"campaign_id": campaign.id, "status": "draft_ready_for_review"}Run this over a CSV of leads and you have one draft campaign per prospect, ready for you to approve in the FoxReach dashboard. When you trust the outputs, add a client.campaigns.start(campaign.id) call at the end and the agent ships fully autonomously.
Multi-step with LangGraph
When the agent needs to loop (retry failed research, wait for reply, classify the reply, branch on intent), graduate to LangGraph. Same FoxReach tools, same SDK, but with explicit state and conditional edges.
A typical LangGraph structure: research → personalize → send_draft → wait for reply → classify_reply → branch on intent (hot / cold / unsubscribe). Each node is a LangChain tool call; the graph is responsible for what happens next. FoxReach webhooks drive the reply node - see the autonomous reply triage pattern for a working LangGraph + FoxReach webhook example.
Rate limits and errors
FoxReach allows 100 API requests per minute per workspace. Under normal agent conversation you will not hit it. When you do batch operations - imports of 500+ leads, bulk sequence creation - use the batch endpoints instead of looping single calls:
client.leads.create_batch- up to 500 leads in one requestclient.leads.update_batch- mass status updatesclient.sequences.create_multiple- an entire sequence as one call
On 429 responses the SDK retries with exponential backoff automatically. LangChain's RetryingToolCallbackHandler adds another retry layer at the agent level if you want it.
Common pitfalls
Fabricated leads
LangChain agents will hallucinate email addresses when asked to find contacts. Always source leads from verified data (your CRM, a scraper you trust, Clay). Never let the agent both find and send to a contact.
Sending from the draft state
FoxReach campaigns start in draft by default. An agent that calls create_campaign and immediately expects leads to receive email is confused. Either call start_campaign explicitly or leave the campaign in draft for human approval.
Overrunning rate limits
At 100 requests/minute, a bulk lead upload loop can hit the limit. Use the bulk endpoints (create_leads_batch) or throttle with a semaphore.
Missing idempotency on retries
LangChain tool calls can retry on transient errors. If your agent creates the same campaign twice, you get duplicates. Pass an idempotency_key header or check for an existing campaign name before creating.
Frequently asked questions
Does the FoxReach Python SDK work with async LangChain agents?
Yes. The SDK ships both sync (foxreach.FoxReach) and async (foxreach.AsyncFoxReach) clients. Use the async client inside a LangChain tool defined with StructuredTool.from_function where the func is an async coroutine, and the tool becomes awaitable across the whole agent graph.
Can I use LangChain agents to reply to inbox messages, not just send?
Yes. The MCP server exposes list_inbox_messages, get_message, and reply_to_message tools. A LangChain agent can triage unread messages, classify intent with an LLM, and reply through FoxReach in the same chain. See the autonomous reply triage pattern in the pillar.
What is the difference between the SDK and MCP for a LangChain agent?
The SDK is a Python library you import and call. You write the tool wrappers that expose FoxReach methods to LangChain. The MCP adapter auto-discovers the 23 tools the FoxReach MCP server exposes and registers them with LangChain via langchain-mcp-adapters. SDK is tighter integration; MCP is less code and future-proof (new FoxReach tools appear automatically).
How do I handle bounces and unsubscribes from LangChain?
FoxReach maintains a suppression list that the agent cannot override. When a recipient unsubscribes or a send bounces permanently, they are blocked from all future campaigns automatically. In the agent loop, check lead.status on get_lead calls - statuses like unsubscribed, bounced, or opted_out mean do not retry.
Can I run this in LangSmith for observability?
Yes. LangSmith traces show the full agent chain including FoxReach tool calls. Wrap your agent in with_config({'run_name': 'cold-email-agent'}) and every run appears in the LangSmith UI with inputs, outputs, latency, and errors per tool call.
See also
Ship your LangChain cold email agent today
Free plan, no credit card. API key in under 60 seconds.