Introduction to CrewAI and Sales Outreach Agents
CrewAI is an open-source framework designed for orchestrating role-playing, autonomous AI agents that collaborate to accomplish complex tasks. Unlike single-agent systems, CrewAI enables you to define a crew of specialized agents—each with distinct roles, goals, and tools—that work together sequentially or hierarchically. A Sales Outreach Agent built with CrewAI automates the end-to-end process of identifying prospects, researching companies, drafting personalized emails, and tracking follow-ups, all while maintaining a natural, human-like tone.
In this complete guide, you'll learn how to build a fully functional sales outreach crew from scratch. We'll cover agent design, tool integration, task orchestration, and production-ready best practices.
What is a Sales Outreach Agent?
A Sales Outreach Agent is an AI-powered system that automates the workflow of finding and engaging potential customers. Within CrewAI, this is typically implemented as a crew composed of multiple specialized agents:
- Lead Researcher Agent — Scrapes or searches for target companies and contacts based on ideal customer profile (ICP) criteria.
- Personalization Agent — Analyzes a prospect's website, LinkedIn profile, or recent news to craft a tailored opening line or value proposition.
- Email Drafter Agent — Composes the full outreach email using proven copywriting frameworks (AIDA, PAS, etc.) and the personalization data.
- Reviewer Agent — Proofreads the draft for grammar, tone, and compliance before it's sent.
- Dispatcher Agent — Handles the actual sending via an email API or schedules the email in a CRM.
Each agent is powered by a large language model (such as GPT-4, Claude, or an open-source model via Ollama) and can be equipped with tools like web scrapers, search engines, and API clients.
Why Building a Sales Outreach Agent Matters
Manual sales outreach is time-consuming, inconsistent, and difficult to scale. Sales development representatives (SDRs) spend hours on repetitive research and email drafting. An AI-powered outreach crew delivers several key benefits:
- Scalability — A crew can research and draft hundreds of personalized emails per day, far exceeding human capacity.
- Consistency — Every email follows your brand guidelines, tone, and compliance rules without deviation.
- Personalization at Scale — Unlike templated mass emails, the agent reads each prospect's actual website and LinkedIn activity to craft genuinely relevant messages.
- Cost Efficiency — Reduces the need for large SDR teams while improving reply rates through higher-quality outreach.
- Continuous Learning — Agents can log outcomes (replies, bounces, unsubscribes) to refine future outreach strategies.
By orchestrating multiple agents with CrewAI, you break down a complex sales process into manageable, parallelizable steps that mirror how a human sales team actually works.
Prerequisites and Setup
Before building your crew, ensure you have the following installed and configured:
- Python 3.10 or higher
- An API key for your preferred LLM provider (OpenAI, Anthropic, or a local Ollama setup)
- Optional: API keys for tools like Serper (Google search), Hunter.io (email finding), or a CRM API
Install CrewAI and its dependencies:
pip install crewai crewai[tools] langchain-openai
Set your environment variables in a .env file:
OPENAI_API_KEY=sk-your-key-here
SERPER_API_KEY=your-serper-key
HUNTER_API_KEY=your-hunter-key
Project Structure
A well-organized CrewAI project separates agent definitions, tasks, tools, and the main execution flow. Here's the recommended structure:
sales_outreach_crew/
├── .env
├── main.py # Entry point that runs the crew
├── agents.py # Agent definitions (roles, goals, backstories)
├── tasks.py # Task definitions and inter-task dependencies
├── tools/
│ ├── __init__.py
│ ├── web_scraper.py # Custom tool for scraping prospect websites
│ ├── linkedin_search.py # Tool for LinkedIn profile lookup
│ └── email_sender.py # Tool for dispatching emails via SMTP or API
└── utils/
├── __init__.py
└── data_models.py # Pydantic models for structured data passing
This modular structure keeps your code maintainable and allows you to swap agents or tools independently as requirements evolve.
Step 1: Defining the Agents
In CrewAI, agents are defined with a Role, Goal, and Backstory. The backstory is crucial—it shapes the agent's personality and decision-making style. Create your agents.py file:
from crewai import Agent
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
from tools.web_scraper import DeepScrapeTool
from tools.email_sender import EmailDispatchTool
# Shared tool instances
search_tool = SerperDevTool()
scrape_tool = ScrapeWebsiteTool()
deep_scrape_tool = DeepScrapeTool()
email_tool = EmailDispatchTool()
# Agent 1: Lead Researcher
lead_researcher = Agent(
role="Senior Lead Research Analyst",
goal="Identify and qualify 5 high-fit prospects based on the provided ICP, "
"retrieving company names, websites, and key decision-maker details.",
backstory=(
"You are a seasoned B2B researcher with 15 years of experience in enterprise SaaS sales. "
"You excel at using search engines and company databases to find prospects that match "
"exactly the ideal customer profile. You always verify company size, industry, and "
"technology stack before adding a prospect to your list. Your research is meticulous "
"and you never include unverified information."
),
tools=[search_tool, scrape_tool],
verbose=True,
allow_delegation=False,
max_iter=3,
llm="gpt-4-turbo"
)
# Agent 2: Personalization Specialist
personalization_agent = Agent(
role="Personalization & Insights Specialist",
goal="For each prospect, extract unique triggers from their website and LinkedIn "
"to craft a compelling personalization hook that demonstrates genuine interest.",
backstory=(
"You are a world-class copywriter and researcher who specializes in finding "
"the 'hook' that makes an outreach email impossible to ignore. You read press "
"releases, blog posts, job listings, and social media to uncover recent triggers "
"like funding rounds, product launches, executive hires, or pain points. "
"You distill each insight into one crisp, specific sentence."
),
tools=[deep_scrape_tool, search_tool],
verbose=True,
allow_delegation=False,
max_iter=5,
llm="gpt-4-turbo"
)
# Agent 3: Email Drafter
email_drafter = Agent(
role="Senior Outbound Sales Email Writer",
goal="Compose a concise, value-driven outreach email for each prospect using the "
"AIDA framework, incorporating the personalization hook naturally.",
backstory=(
"You are an elite B2B sales copywriter who has written thousands of cold emails "
"with industry-leading reply rates (15-25%). You follow the AIDA framework "
"(Attention, Interest, Desire, Action) religiously. Your emails are never longer "
"than 6 sentences, always include a specific value proposition, and end with "
"a low-friction call-to-action like a 15-minute call or a specific question. "
"You avoid marketing fluff and jargon entirely."
),
tools=[],
verbose=True,
allow_delegation=False,
llm="gpt-4-turbo"
)
# Agent 4: Quality Reviewer
reviewer_agent = Agent(
role="Email Quality Assurance Specialist",
goal="Review each draft email for grammar errors, tone issues, compliance with "
"anti-spam regulations, and alignment with the original personalization hook.",
backstory=(
"You are a meticulous editor with 10 years of experience in B2B communications. "
"You check every email for: 1) Grammatical errors, 2) Tone that matches the "
"brand voice (professional yet warm), 3) CAN-SPAM/GDPR compliance (valid unsubscribe "
"language, accurate sender info), and 4) Faithfulness to the personalization research. "
"If an email fails any check, you revise it or flag it for human review."
),
tools=[],
verbose=True,
allow_delegation=False,
llm="gpt-4-turbo"
)
# Agent 5: Dispatcher
dispatcher_agent = Agent(
role="Email Dispatch & CRM Manager",
goal="Send approved emails to prospects and log all activities in the CRM for tracking.",
backstory=(
"You are a reliable automation specialist who manages the final delivery step. "
"You integrate with the email API and CRM system. You handle rate limiting, "
"tracking pixel insertion, and proper threading. You log every send attempt, "
"success, and failure with timestamps."
),
tools=[email_tool],
verbose=True,
allow_delegation=False,
llm="gpt-4-turbo"
)
Notice how each agent has a deeply specific backstory. This isn't just flavor text—CrewAI uses the backstory to condition the LLM's behavior, making agents consistently follow their designated expertise.
Step 2: Defining the Tasks
Tasks in CrewAI represent units of work assigned to agents. They can have dependencies (context) that pass outputs from previous tasks forward. Create tasks.py:
from crewai import Task
from agents import (
lead_researcher, personalization_agent, email_drafter,
reviewer_agent, dispatcher_agent
)
# Task 1: Research Prospects
research_task = Task(
description=(
"Given the ICP: 'Series A to C B2B SaaS companies in the DevOps and cloud infrastructure '
"space, with 50-200 employees, headquartered in North America, using Kubernetes and at "
"least one major cloud provider.'\n\n"
"1. Use Serper to search for companies matching this ICP.\n"
"2. For each promising result, scrape the company website to verify employee count, "
" technology stack indicators, and recent news.\n"
"3. Identify the VP of Engineering, CTO, or Head of DevOps as the decision-maker.\n"
"4. Compile a structured list with: company name, website, decision-maker name/title, "
" verified ICP match criteria.\n"
"5. Output exactly 5 qualified prospects."
),
expected_output=(
"A JSON list of 5 qualified prospects, each containing:\n"
"- company_name: string\n"
"- website: string\n"
"- decision_maker_name: string\n"
"- decision_maker_title: string\n"
"- icp_match_notes: string (explaining why they fit)\n"
"- linkedin_url: string (if found)"
),
agent=lead_researcher,
async_execution=False
)
# Task 2: Personalization Research
personalization_task = Task(
description=(
"For each prospect identified in the research phase, perform deep personalization research:\n"
"1. Scrape the company's 'About Us', 'Blog', and 'Press/News' pages.\n"
"2. Search for recent news about the company (funding, product launches, partnerships).\n"
"3. If available, look up the decision-maker's LinkedIn profile for shared connections, "
" recent posts, or published articles.\n"
"4. Extract exactly ONE specific personalization trigger per prospect—something that "
" shows you've done genuine research.\n"
"5. For each trigger, write a one-sentence 'hook' that can be used in the email opening."
),
expected_output=(
"For each of the 5 prospects, provide:\n"
"- prospect_company: string\n"
"- trigger_source: string (URL where the trigger was found)\n"
"- personalization_hook: string (one specific, compelling sentence)\n"
"- relevance_rationale: string (why this hook matters to the prospect)"
),
agent=personalization_agent,
context=[research_task], # Receives output from research_task
async_execution=False
)
# Task 3: Draft Emails
drafting_task = Task(
description=(
"Using the prospect research and personalization hooks, draft one outreach email per prospect:\n"
"1. Subject line: Must be specific and curiosity-driven (under 50 characters).\n"
"2. Opening: Incorporate the personalization hook naturally.\n"
"3. Body: State your value proposition clearly—how your product solves a pain point "
" relevant to their DevOps/cloud infrastructure context.\n"
"4. CTA: A single, low-friction ask (e.g., 'Open to a 15-minute call next week?' "
" or 'Would love your take on [specific topic].').\n"
"5. Signature: Your name, title, company, and a professional signature.\n"
"6. Total email length: 4-6 sentences maximum."
),
expected_output=(
"For each of the 5 prospects, provide:\n"
"- prospect_name: string\n"
"- subject_line: string\n"
"- email_body: string (the complete email)\n"
"- cta: string\n"
"- framework_used: string (AIDA breakdown showing each component)"
),
agent=email_drafter,
context=[personalization_task], # Receives output from personalization_task
async_execution=False
)
# Task 4: Review Emails
review_task = Task(
description=(
"Review every drafted email against quality standards:\n"
"1. Grammar & spelling: Run a thorough check.\n"
"2. Tone: Ensure professional yet warm; no pushy sales language.\n"
"3. Compliance: Verify CAN-SPAM elements (physical address, unsubscribe option).\n"
"4. Hook fidelity: Confirm the personalization hook is accurately used.\n"
"5. For any email that fails review, revise it directly.\n"
"6. Output the final, approved version of each email ready for dispatch."
),
expected_output=(
"For each of the 5 prospects, provide:\n"
"- prospect_name: string\n"
"- review_status: 'approved' or 'revised'\n"
"- final_subject_line: string\n"
"- final_email_body: string\n"
"- reviewer_notes: string (changes made or reason for approval)"
),
agent=reviewer_agent,
context=[drafting_task],
async_execution=False
)
# Task 5: Dispatch Emails
dispatch_task = Task(
description=(
"Send the approved emails to prospects:\n"
"1. For each approved email, use the email dispatch tool to send it.\n"
"2. Insert tracking pixel for open rate monitoring.\n"
"3. Log the send status (success/failure) with timestamp.\n"
"4. Handle rate limiting—send no more than 1 email every 30 seconds.\n"
"5. At the end, provide a summary of sends: total attempted, successful, failed."
),
expected_output=(
"A dispatch summary containing:\n"
"- total_attempted: integer\n"
"- successful: integer\n"
"- failed: integer\n"
"- failure_details: list of failed sends with reasons\n"
"- timestamp_utc: string"
),
agent=dispatcher_agent,
context=[review_task],
async_execution=False
)
The context parameter is the key to CrewAI's orchestration. By setting context=[research_task] on the personalization task, CrewAI automatically feeds the research output into the personalization agent's prompt. This creates a sequential pipeline where each step builds on the previous one.
Step 3: Building Custom Tools
While CrewAI provides built-in tools like SerperDevTool and ScrapeWebsiteTool, you'll often need custom tools for deep scraping, LinkedIn research, or email dispatch. Here's how to build a custom deep scraper tool using LangChain's BaseTool:
# tools/web_scraper.py
from crewai_tools import BaseTool
from typing import Optional, Type
from pydantic import BaseModel, Field
import requests
from bs4 import BeautifulSoup
import time
class DeepScrapeInput(BaseModel):
"""Input schema for the deep scrape tool."""
url: str = Field(..., description="The URL of the page to scrape")
extract_links: bool = Field(
default=False,
description="Whether to also extract and return all hyperlinks on the page"
)
class DeepScrapeTool(BaseTool):
name: str = "Deep Website Scraper"
description: str = (
"Scrapes a web page and extracts all text content, meta tags, and optionally "
"all hyperlinks. Use this when you need comprehensive content from a specific "
"page (About Us, Blog, Press, etc.) for personalization research."
)
args_schema: Type[BaseModel] = DeepScrapeInput
request_timeout: int = 15
def _run(self, url: str, extract_links: bool = False) -> str:
"""
Execute the scraping operation.
"""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
'AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/120.0.0.0 Safari/537.36'
}
response = requests.get(url, headers=headers, timeout=self.request_timeout)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# Remove script and style elements
for script in soup(['script', 'style', 'nav', 'footer']):
script.decompose()
# Extract meta tags
meta_tags = {}
for meta in soup.find_all('meta'):
name = meta.get('name') or meta.get('property')
content = meta.get('content')
if name and content:
meta_tags[name] = content
# Extract main content
main_content = []
for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'li', 'span', 'div']):
text = tag.get_text(strip=True)
if text and len(text) > 20: # Filter out tiny fragments
main_content.append(text)
result = {
'url': url,
'title': soup.title.string if soup.title else 'No title',
'meta_tags': meta_tags,
'content': '\n'.join(main_content[:100]), # Limit to 100 paragraphs
'word_count': sum(len(p.split()) for p in main_content[:100])
}
if extract_links:
links = []
for a in soup.find_all('a', href=True):
href = a['href']
if href.startswith('http') or href.startswith('/'):
links.append({
'text': a.get_text(strip=True)[:100],
'url': href
})
result['links'] = links[:50] # Limit to 50 links
return str(result)
except requests.RequestException as e:
return f"Error scraping {url}: {str(e)}"
except Exception as e:
return f"Unexpected error: {str(e)}"
async def _arun(self, url: str, extract_links: bool = False) -> str:
"""Async version (falls back to sync for simplicity)."""
time.sleep(0) # Allow event loop to yield
return self._run(url, extract_links)
Now build the email dispatch tool. For production, you'd integrate with an actual email API like SendGrid, Resend, or your SMTP server:
# tools/email_sender.py
from crewai_tools import BaseTool
from typing import Type
from pydantic import BaseModel, Field
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os
import time
class EmailDispatchInput(BaseModel):
"""Input schema for email dispatch."""
to_email: str = Field(..., description="Recipient email address")
to_name: str = Field(..., description="Recipient full name")
subject: str = Field(..., description="Email subject line")
body: str = Field(..., description="Plain text email body")
tracking_enabled: bool = Field(
default=True,
description="Whether to insert an open-tracking pixel"
)
class EmailDispatchTool(BaseTool):
name: str = "Email Dispatch Tool"
description: str = (
"Sends a single email via SMTP. Includes optional open tracking pixel. "
"Respects rate limiting—call this no more than once every 30 seconds."
)
args_schema: Type[BaseModel] = EmailDispatchInput
# Rate limiting state
_last_send_time: float = 0.0
_min_interval: float = 30.0 # seconds between sends
def _run(self, to_email: str, to_name: str, subject: str, body: str,
tracking_enabled: bool = True) -> str:
"""
Send an email with rate limiting.
"""
# Enforce rate limiting
elapsed = time.time() - self._last_send_time
if elapsed < self._min_interval:
wait_time = self._min_interval - elapsed
time.sleep(wait_time)
try:
smtp_host = os.getenv('SMTP_HOST', 'smtp.sendgrid.net')
smtp_port = int(os.getenv('SMTP_PORT', '587'))
smtp_user = os.getenv('SMTP_USER')
smtp_password = os.getenv('SMTP_PASSWORD')
from_email = os.getenv('FROM_EMAIL', 'sales@yourcompany.com')
from_name = os.getenv('FROM_NAME', 'Alex from YourCo')
# Build message
msg = MIMEMultipart('alternative')
msg['From'] = f"{from_name} <{from_email}>"
msg['To'] = f"{to_name} <{to_email}>"
msg['Subject'] = subject
msg['Reply-To'] = from_email
# Add tracking pixel if enabled
if tracking_enabled:
tracking_id = f"{to_email}@{int(time.time())}"
pixel_url = f"https://track.yourcompany.com/pixel/{tracking_id}"
html_body = f"""
{body.replace(chr(10), '
')}
Register these tools in your tools/__init__.py:
from .web_scraper import DeepScrapeTool
from .email_sender import EmailDispatchTool
__all__ = ['DeepScrapeTool', 'EmailDispatchTool']
Step 4: Assembling and Running the Crew
The final step is to create the crew, assign agents to tasks, and execute the workflow. Create main.py:
# main.py
from crewai import Crew, Process
from agents import (
lead_researcher, personalization_agent, email_drafter,
reviewer_agent, dispatcher_agent
)
from tasks import (
research_task, personalization_task, drafting_task,
review_task, dispatch_task
)
from dotenv import load_dotenv
import json
import datetime
load_dotenv()
# Assemble the crew
sales_outreach_crew = Crew(
agents=[
lead_researcher,
personalization_agent,
email_drafter,
reviewer_agent,
dispatcher_agent
],
tasks=[
research_task,
personalization_task,
drafting_task,
review_task,
dispatch_task
],
process=Process.sequential, # Execute tasks one after another
verbose=True, # Full logging of agent thoughts and actions
memory=True, # Enable crew-level memory across tasks
planning=True, # Enable planning step before execution
max_rpm=10, # Rate limit: max 10 LLM requests per minute
share_crew=True # Allow agents to share context
)
def run_outreach_campaign():
"""
Execute the full sales outreach crew pipeline.
"""
print("=" * 60)
print(f"🚀 Starting Sales Outreach Campaign — {datetime.datetime.now().isoformat()}")
print("=" * 60)
# Kick off the crew
result = sales_outreach_crew.kickoff()
print("\n" + "=" * 60)
print("✅ Campaign Complete!")
print("=" * 60)
# Save results to a log file
output_file = f"campaign_results_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
with open(output_file, 'w') as f:
json.dump({
'timestamp': datetime.datetime.now().isoformat(),
'crew_output': str(result),
'agent_outputs': {
'research': research_task.output.raw if hasattr(research_task.output, 'raw') else str(research_task.output),
'personalization': personalization_task.output.raw if hasattr(personalization_task.output, 'raw') else str(personalization_task.output),
'drafting': drafting_task.output.raw if hasattr(drafting_task.output, 'raw') else str(drafting_task.output),
'review': review_task.output.raw if hasattr(review_task.output, 'raw') else str(review_task.output),
'dispatch': dispatch_task.output.raw if hasattr(dispatch_task.output, 'raw') else str(dispatch_task.output),
}
}, f, indent=2, default=str)
print(f"📁 Detailed results saved to {output_file}")
return result
if __name__ == "__main__":
run_outreach_campaign()
Let's break down the key parameters in the Crew constructor:
process=Process.sequential— Ensures tasks execute in the order defined, with each task receiving the output of the previous one viacontext. For more complex workflows, you can useProcess.hierarchicalwhere a manager agent dynamically assigns work.verbose=True— Logs every agent's internal reasoning, tool calls, and outputs. Essential for debugging and understanding what your crew is actually doing.memory=True— Enables short-term memory so agents can reference earlier findings across the entire crew run.planning=True— Adds a planning step where the crew analyzes all tasks before execution, improving coordination.max_rpm=10— Prevents hitting LLM API rate limits by throttling requests.
Step 5: Testing Locally with Ollama (Optional)
For local development without consuming paid API credits, you can use Ollama with open-source models. Install Ollama and pull a model:
# Install Ollama (macOS/Linux)
curl -fsSL https://ollama.com/install.sh | sh
# Pull a capable model
ollama pull llama3.1:8b
ollama pull mistral-nemo:12b
Then modify your agent definitions to use the local model:
# Change the llm parameter in each agent
lead_researcher = Agent(
# ... other parameters unchanged ...
llm="ollama/llama3.1:8b"
)
For production, you'll want a more powerful model like GPT-4 or Claude, but Ollama is perfect for iterating on agent logic and prompt engineering without costs.
Best Practices for Production Sales Outreach Crews
1. Start with a Narrow ICP
One of the most common mistakes is casting too wide a net. A tightly defined ICP—specific industry, company size, technology stack, and geography—dramatically improves the relevance of both the research and the personalization. Start with a niche where you have proven product-market fit, then expand gradually.
2. Implement Human-in-the-Loop Review
Even with a reviewer agent, critical emails should have a human checkpoint. Modify the dispatch task to only send emails marked approved by the reviewer, and create a separate queue for emails flagged as needs_human_review. This prevents embarrassing mistakes from reaching real prospects.
3. Use Structured Outputs with Pydantic
Define Pydantic models for the data passing between tasks. This ensures type safety and prevents agents from producing malformed outputs that break downstream tasks:
# utils/data_models.py
from pydantic import BaseModel
from typing import List, Optional
class Prospect(BaseModel):
company_name: str
website: str
decision_maker_name: str
decision_maker_title: str
icp_match_notes: str
linkedin_url: Optional[str] = None
class PersonalizationHook(BaseModel):
prospect_company: str
trigger_source: str
personalization_hook: str
relevance_rationale: str
class OutreachEmail(BaseModel):
prospect_name: str
subject_line: str
email_body: str
cta: str
framework_used: str
class CampaignResult(BaseModel):
prospects: List[Prospect]
hooks: List[PersonalizationHook]
emails: List[OutreachEmail]
dispatch_summary: dict
Use these models in your task expected_output descriptions to guide the agents toward structured, parseable outputs.
4. Monitor and Log Everything
Sales outreach touches real people, so thorough logging is non-negotiable. Log every agent's output, every tool call, every email sent, and every error. Store logs in a structured format (JSON or a database) so you can audit exactly what your crew did and when. This is also critical for debugging—when a prospect replies negatively, you need to trace back exactly what personalization hook was used.
5. Implement Graceful Error Handling
Agents will encounter errors: websites may be down, search APIs may rate-limit, email servers may reject connections. Wrap critical tool calls in try-except blocks and return structured error messages that the agent can reason about. CrewAI agents are surprisingly good at adapting when they receive clear error feedback.
6. Respect Email Regulations
Ensure your reviewer agent explicitly checks for CAN-SPAM (US) and GDPR (EU) compliance. Every email must include a valid physical mailing address and a clear unsubscribe mechanism. If you're emailing EU residents, you need a lawful basis for processing their data. Build these checks into your review task description.
7. Tune Agent Backstories Iteratively
The backstory is the most powerful lever for shaping agent behavior. If an agent consistently produces output that's too verbose, too generic, or misses key details, refine its backstory. Add specific instructions about output format, tone, and what to avoid. Treat backstories like prompts—iterate on them based on observed outputs.
8. Scale Gradually with Rate Limiting
When scaling from 5 prospects to hundreds, respect both your email provider's rate limits and the LLM API's rate limits. CrewAI's max_rpm handles LLM rate limiting, but you need to implement email sending delays (as shown in the EmailDispatchTool) to avoid being flagged as spam. A safe starting point is 1 email every 30-60 seconds.
9. A/B Test Your Outreach
Treat your crew as an experimentation platform. Create variants of your email drafter agent with different backstories (e.g., one using AIDA, another using the "Problem-Agitate-Solve" framework) and run parallel campaigns to compare reply rates. Use the structured logging to analyze which approach performs better.
Conclusion
Building a Sales Outreach Agent with CrewAI transforms a traditionally manual, time-consuming sales process into an automated, scalable, and continuously improving system. By decomposing the outreach workflow into specialized agents—researcher, personalizer, drafter, reviewer, and dispatcher—you mirror the structure of a high-performing human sales