CrewAI Cold Email: Build a Sales Crew with FoxReach
Three agents - Researcher, Copywriter, Sender - plus FoxReach as the outbound infrastructure. Role-based cold email outreach that handles one lead or a thousand, with human approval gated at the send step until you trust the drafts.
Usama Navid
Founder, FoxReach
What you'll build
A CrewAI crew with three specialized agents that take a list of leads and produce a draft cold email campaign in FoxReach, one personalized message per lead. Crew runs end-to-end in under a minute per lead on a modest LLM, with explicit task handoffs and observable context between agents.
- A Researcher agent that fetches context on each lead
- A Copywriter agent that turns research into a personalized email
- A Sender agent that creates the FoxReach campaign, sequence, and lead
- Task handoffs with typed context the whole crew can inspect
- Human approval gate at the send step (by default)
Prerequisites
Python 3.10+
CrewAI 0.80+ requires 3.10 or newer. 3.11 is a good middle ground.
A FoxReach account and API key
Free plan works. Generate an API key from Settings → API Keys. Prefix: fr_.
An LLM provider
OpenAI, Anthropic, or any LiteLLM-compatible provider. Examples below use gpt-4o-mini for cost and GPT-4o for the copywriter.
A web search provider (optional)
Tavily, Serper, Exa, or your own scraper for the Researcher. Skippable if you seed richer lead data up front.
The crew shape
CrewAI models agents as roles with goals, not as generic tool-callers. Each agent has a narrow responsibility, a defined output format, and a list of tools it can call. Tasks flow sequentially by default; output of task N is available as context to task N+1.
The three-agent pattern below maps well to cold email because each step has a different prompting style: research is structured extraction, copywriting is creative, sending is tool-calling. Trying to do all three in one prompt yields worse output than splitting.
Register FoxReach as a tool
Two ways to expose FoxReach to CrewAI: wrap the Python SDK as custom tools, or use the MCP adapter to auto-register all 23 FoxReach tools. Start with SDK wrappers - they're explicit and your crew prompts can reference specific tools by name.
pip install crewai crewai-tools foxreachimport os
from crewai.tools import tool
from foxreach import FoxReach
client = FoxReach(api_key=os.environ["FOXREACH_API_KEY"])
@tool("Create a draft cold email campaign")
def create_campaign(name: str) -> str:
"""Create a new draft campaign in FoxReach. Returns the campaign ID."""
campaign = client.campaigns.create(name=name, status="draft")
return f"campaign_id={campaign.id}"
@tool("Add a sequence step to a campaign")
def add_sequence_step(campaign_id: str, subject: str, body: str, wait_days: int = 0) -> str:
"""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 f"step_id={step.id} subject={step.subject!r}"
@tool("Import a lead into a campaign")
def import_lead(campaign_id: str, email: str, first_name: str, company: str) -> str:
"""Add a single lead to a campaign."""
lead = client.leads.create(
campaign_id=campaign_id, email=email, first_name=first_name, company=company,
)
return f"lead_id={lead.id}"If you prefer auto-discovery over hand-writing wrappers, use MCPServerAdapter from crewai-tools pointed at api.foxreach.io/mcp. Less code, every new FoxReach tool appears automatically, slightly less control over naming.
Define the agents
Agent 1: Researcher
Takes a lead and returns a structured research note. Focus on what a copywriter can actually use - recent news, role seniority, company stage - not unstructured scrape dumps.
from crewai import Agent
from crewai_tools import SerperDevTool
search = SerperDevTool()
researcher = Agent(
role="Outbound Researcher",
goal="Find the single most specific and recent fact about a lead that justifies a personalized opener.",
backstory="You read LinkedIn, Crunchbase, and TechCrunch daily. You know what makes a cold open land - a real project, a real promotion, a real funding round. You never invent. If the research is thin, you say so.",
tools=[search],
llm="gpt-4o-mini",
verbose=True,
)Agent 2: Copywriter
Takes research + a base template and writes the email. Give it a stronger model than the researcher - tone carries more than facts here.
from crewai import Agent
copywriter = Agent(
role="Cold Email Copywriter",
goal="Write a 60-90 word cold email that feels written to one person, not blasted to a list.",
backstory="You've written a million outbound emails and unsubscribed from every bad one. You open with one concrete observation, then a one-line value prop, then a soft ask. No 'hope this finds you well', no multi-paragraph intros, no questions that aren't answerable.",
tools=[], # No tools - pure text in, text out.
llm="gpt-4o",
verbose=True,
)Agent 3: Sender
Takes the finished email and lead data, then calls FoxReach to create the campaign, add the sequence step, and import the lead. This agent does almost no LLM reasoning - its job is to call the right tools in order.
from crewai import Agent
from foxreach_tools import create_campaign, add_sequence_step, import_lead
sender = Agent(
role="Outbound Sender",
goal="Create a FoxReach campaign in draft state with one personalized sequence step and one lead. Never call start_campaign - that's for humans.",
backstory="You move drafts into FoxReach as drafts. You always name the campaign after the lead's company and the date. You never edit the copy the copywriter gave you.",
tools=[create_campaign, add_sequence_step, import_lead],
llm="gpt-4o-mini",
verbose=True,
)Wire the crew
Tie the three agents to three tasks and let the crew run. Each task's output flows into the next task's context.
from crewai import Crew, Task, Process
from researcher import researcher
from copywriter import copywriter
from sender import sender
lead = {
"name": "Alex Rivera",
"title": "Head of Growth",
"company": "Nuvoform",
"email": "alex@nuvoform.ai",
}
research_task = Task(
description=f"Research {lead['name']}, {lead['title']} at {lead['company']}. Return one specific, recent, citable fact about them or their company.",
expected_output="A 40-80 word research note with one specific fact and its source.",
agent=researcher,
)
copy_task = Task(
description=f"""Using the research, write a cold email to {lead['name']} for Nuvoform. 60-90 words. Open with the research fact. Close with 'worth a quick chat?' or similar soft ask.""",
expected_output="JSON with subject (max 60 chars) and body (60-90 words).",
agent=copywriter,
context=[research_task],
)
send_task = Task(
description=f"Create a FoxReach campaign named 'Nuvoform - {lead['name']}'. Add the sequence step from the copywriter's output. Import lead {lead['email']} ({lead['name']}, {lead['company']}).",
expected_output="campaign_id and lead_id from FoxReach.",
agent=sender,
context=[copy_task],
)
crew = Crew(
agents=[researcher, copywriter, sender],
tasks=[research_task, copy_task, send_task],
process=Process.sequential,
verbose=True,
)
result = crew.kickoff()
print(result)Run this and you get one FoxReach campaign in draft. Open the FoxReach dashboard, review the copywriter's output, start the campaign when it looks right. When you trust the drafts, move the start call into the Sender agent.
Running at scale
One crew per lead is simple. For 500 leads, parallelize instead of letting the crew grow linearly:
import asyncio
from crewai import Crew, Task, Process
async def ship_one(lead: dict) -> dict:
# Build per-lead tasks (same shape as above).
crew = Crew(
agents=[researcher, copywriter, sender],
tasks=[...],
process=Process.sequential,
)
return crew.kickoff() # CrewAI handles the concurrent LLM calls internally.
async def ship_many(leads: list[dict]) -> list[dict]:
# Cap concurrency so we don't blow past FoxReach's 100 req/min rate limit.
sem = asyncio.Semaphore(10)
async def guarded(lead):
async with sem:
return await asyncio.to_thread(ship_one, lead)
return await asyncio.gather(*(guarded(lead) for lead in leads))Keep the semaphore at 10-20 for workspaces on the FoxReach free or starter plan. Higher tiers raise the rate limit - check your dashboard. If you hit 429 responses, the SDK retries with exponential backoff automatically, so the crew recovers without code changes.
Common pitfalls
Agents improvising email addresses
If the researcher agent returns an email address that was not passed in, reject it. CrewAI agents will fabricate when asked to "find" a contact. Always seed the crew with verified email addresses from your CRM or a trusted scraper.
Campaign name collisions
Two crews running the same day will both try to create a campaign called "Outbound - 2026-04-17". The second call succeeds but creates a duplicate. Namespace with a run ID (UUID, timestamp) or check for an existing campaign first.
Letting the sender agent call start_campaign
Default to draft. Only graduate the sender agent to fully autonomous sends after you review the first 50-100 drafts it produces. Swap the start_campaign tool for a Slack-notify tool until then.
Unbounded task lists
If you pass 500 leads into one crew run, the crew becomes a 500-step chain with no recovery. Shard the input: one crew per 20 leads, parallelize with asyncio.gather.
Frequently asked questions
How is CrewAI different from LangChain for cold email?
CrewAI is role-based: you define agents (Researcher, Writer, Sender) with goals and personas, and the framework coordinates task handoffs. LangChain is tool-based: one agent with a toolkit. Use CrewAI when outreach is one role in a larger pipeline. Use LangChain when you want maximum control over the agent loop.
Can I mix CrewAI and the FoxReach MCP server?
Yes. Install crewai-tools and MCPServerAdapter - it turns any MCP server into CrewAI tools. Point it at https://api.foxreach.io/mcp and every FoxReach tool appears in the tool list. Cleaner than wrapping the SDK by hand if you want auto-discovery of new tools.
Does CrewAI support async tools with FoxReach?
Yes. FoxReach's AsyncFoxReach client works inside async tool functions. CrewAI's Process.hierarchical mode awaits tool results correctly. Stay sync unless you actually need concurrent tool calls - for most cold email crews, sync is simpler and fast enough.
Which LLM should I use for each agent?
Mix. Researcher can use a cheaper model (GPT-4o-mini, Claude Haiku) because its output is compressed into structured research notes. Copywriter benefits from a stronger model (GPT-4o, Claude Sonnet) because tone is everything. Sender is mostly tool-calling and doesn't need a big brain - default to whatever the crew uses.
How do I handle replies in a CrewAI setup?
Add a fourth agent - Triager - and run it on a webhook trigger when FoxReach fires the reply_received event. The Triager reads the reply, classifies intent (hot/cold/unsubscribe/out-of-office), and either drafts a reply via FoxReach or flags for human. See the autonomous reply triage pattern in the pillar guide.
See also
Ship your CrewAI sales crew today
Free plan, no credit card. First crew campaign in under 30 minutes.