FoxReach
Framework Integration

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.

bashshell
pip install foxreach langchain langchain-openai
foxreach_tools.pypython
import 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.

bashshell
pip install langchain-mcp-adapters langchain langchain-openai
mcp_agent.pypython
import 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.

research.pypython
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.

personalize.pypython
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.

pipeline.pypython
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 request
  • client.leads.update_batch - mass status updates
  • client.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.