Introduction: What is a Sales Outreach Agent with AutoGen?
A Sales Outreach Agent built with AutoGen is an AI-powered multi-agent system designed to automate and enhance the process of prospecting, researching, personalizing, and executing sales outreach campaigns. AutoGen, developed by Microsoft Research, provides a framework for creating conversational agents that can collaborate, reason, and take actions—making it an ideal foundation for building sophisticated sales automation workflows.
Rather than relying on a single monolithic AI model, an AutoGen-based sales outreach system distributes responsibilities across specialized agents: one researches target companies, another crafts personalized messages, a third manages email composition and scheduling, and a fourth handles follow-ups. These agents communicate with each other, share insights, and iteratively refine their outputs—mimicking the collaboration of a human sales team but operating at machine speed and scale.
Why AutoGen for Sales Outreach Matters
Traditional sales outreach faces three persistent challenges: scalability (manually researching and personalizing hundreds of prospects is time-prohibitive), consistency (human fatigue leads to generic templated messages), and intelligence (static sequences cannot adapt to real-time signals). AutoGen addresses all three by enabling:
- Dynamic agent collaboration — Agents can debate, revise, and approve each other's work before execution
- Tool integration — Agents can call external APIs (CRM lookups, LinkedIn scraping, email services) via function calling
- Human-in-the-loop oversight — A human reviewer agent can intercept and approve sensitive actions before they execute
- Contextual memory — Conversation history persists across interactions, enabling multi-turn refinement
- Cost-effective orchestration — Different tasks can be routed to appropriately sized models (e.g., GPT-4o for strategy, GPT-3.5 for formatting)
The result is a system that can take a CSV of 200 target accounts, research each one, generate personalized opening messages, and schedule them for delivery—all while a human sales manager reviews and approves the final output.
Setting Up Your Environment
Before building the agent system, install the required dependencies. You'll need the autogen library along with supporting packages for API calls and email handling.
# Create and activate a virtual environment (recommended)
python -m venv sales-agent-env
source sales-agent-env/bin/activate # On Windows: sales-agent-env\Scripts\activate
# Install core dependencies
pip install pyautogen
pip install openai
pip install requests
pip install beautifulsoup4
pip install sendgrid # For email delivery
pip install python-dotenv
Set up your environment variables in a .env file:
# .env file
OPENAI_API_KEY=sk-your-openai-api-key-here
SENDGRID_API_KEY=your-sendgrid-api-key
CRM_API_KEY=your-crm-api-key
LINKEDIN_API_KEY=your-linkedin-api-key # Optional, for enrichment
Load these variables at the start of your application:
import os
from dotenv import load_dotenv
load_dotenv()
openai_api_key = os.getenv("OPENAI_API_KEY")
sendgrid_api_key = os.getenv("SENDGRID_API_KEY")
Core Architecture: Multi-Agent Design Pattern
The sales outreach system employs a supervisor-worker pattern with specialized agents, orchestrated through AutoGen's GroupChat and managed by a GroupChatManager. Here's the agent topology:
- Research Agent — Gathers intelligence on target companies and contacts using web search, CRM data, and enrichment APIs
- Personalization Agent — Analyzes research findings and crafts tailored messaging hooks based on triggers, pain points, and recent news
- Copywriter Agent — Composes the full outreach email with proper tone, structure, and call-to-action
- Reviewer Agent — Performs quality assurance: checks grammar, verifies factual claims, and scores the email against best-practice rubrics
- Dispatcher Agent — Handles the actual sending via email API and logs outcomes to the CRM
- Human Proxy Agent — Represents the human sales manager who gives final approval before dispatch
Communication flows sequentially: Research → Personalization → Copywriter → Reviewer → Human Approval → Dispatch. However, agents can also engage in multi-turn debate if the Reviewer identifies issues that require revision.
Building the Sales Outreach Agent: Step-by-Step
Step 1: Define Agent Configurations and LLM Settings
AutoGen uses configuration dictionaries to define each agent's LLM backend, system prompt, and tool set. Start by creating the shared configuration:
import autogen
# LLM configuration for different agent tiers
# Use GPT-4o for high-reasoning tasks, GPT-3.5-turbo for formatting
config_list = autogen.config_list_from_json(
env_or_file="OAI_CONFIG_LIST",
file_location=".",
filter_dict={
"model": ["gpt-4o", "gpt-3.5-turbo"],
},
)
# Tiered configurations
research_llm_config = {
"config_list": config_list,
"model": "gpt-4o",
"temperature": 0.3,
"timeout": 120,
}
creative_llm_config = {
"config_list": config_list,
"model": "gpt-4o",
"temperature": 0.7,
"timeout": 120,
}
utility_llm_config = {
"config_list": config_list,
"model": "gpt-3.5-turbo",
"temperature": 0.2,
"timeout": 60,
}
Step 2: Create the Research Agent with Tool Integration
The Research Agent needs the ability to search the web and query your CRM. In AutoGen, tools are registered as functions that the agent can invoke during conversation. Here's how to build it:
import requests
from bs4 import BeautifulSoup
# Define tool functions that the Research Agent can call
def search_company_news(company_name: str) -> str:
"""
Search for recent news about a company using a news API.
Returns a summary of relevant news articles from the last 30 days.
"""
# Simulated API call — replace with actual NewsAPI or similar
search_url = f"https://newsapi.org/v2/everything?q={company_name}&sortBy=publishedAt&pageSize=5"
# In production, use your actual API key and error handling
try:
response = requests.get(
search_url,
headers={"X-Api-Key": os.getenv("NEWS_API_KEY")},
timeout=10
)
if response.status_code == 200:
articles = response.json().get("articles", [])
summaries = [f"- {art['title']}: {art['description'][:200]}" for art in articles[:5]]
return "Recent news:\n" + "\n".join(summaries) if summaries else "No recent news found."
return f"News API returned status {response.status_code}"
except Exception as e:
return f"Error searching news: {str(e)}"
def lookup_crm_contact(company_name: str) -> str:
"""
Look up a company in the CRM to find existing contacts, past interactions,
and pipeline status. Returns structured CRM data.
"""
# Simulated CRM lookup — replace with HubSpot/Salesforce API calls
# In production, query your actual CRM here
crm_data = {
"company": company_name,
"contacts": [
{"name": "Sarah Chen", "title": "VP Engineering", "last_contacted": "2024-09-15"},
{"name": "Marcus Webb", "title": "CTO", "last_contacted": None}
],
"pipeline_status": "Prospecting",
"past_notes": "Interested in API scalability solutions per Q2 call."
}
return str(crm_data)
# Register these as AutoGen tools
research_tools = [
{
"function": search_company_news,
"name": "search_company_news",
"description": "Search for recent news articles about a given company name."
},
{
"function": lookup_crm_contact,
"name": "lookup_crm_contact",
"description": "Look up CRM records for a company to find contacts and history."
}
]
# Create the Research Agent
research_agent = autogen.AssistantAgent(
name="ResearchAgent",
system_message="""You are a sales research specialist. Your job is to gather intelligence
on target companies and contacts. For each company you're given:
1. Call search_company_news to find recent news, funding announcements, product launches, or challenges.
2. Call lookup_crm_contact to check existing CRM records and past interactions.
3. Synthesize findings into a concise research brief (max 300 words) highlighting:
- Recent triggers (news, funding, hiring, expansion)
- Potential pain points or opportunities
- Existing relationship status from CRM
- Recommended outreach angle
Always cite your sources. Be thorough but concise.""",
llm_config=research_llm_config,
function_map={
"search_company_news": search_company_news,
"lookup_crm_contact": lookup_crm_contact,
}
)
Step 3: Create the Personalization Agent
The Personalization Agent takes the research brief and extracts specific hooks that will resonate with the prospect. It identifies "why you're reaching out now" and "what's in it for them."
personalization_agent = autogen.AssistantAgent(
name="PersonalizationAgent",
system_message="""You are a sales personalization expert. You receive a research brief
from the ResearchAgent and your job is to identify 2-3 compelling personalization hooks.
For each hook, provide:
- **Trigger Event**: What specific event or signal makes this timely?
- **Value Proposition**: How can our product/service help address their likely need?
- **Personal Connection**: How does this tie to the specific contact's role or past interactions?
- **Icebreaker Sentence**: A draft opening line for the email.
Format your output as a structured "Personalization Brief" that the CopywriterAgent
will use to compose the final email. Keep each hook to 3-4 sentences maximum.
Be specific—avoid generic flattery. Reference actual facts from the research.""",
llm_config=creative_llm_config,
)
Step 4: Create the Copywriter, Reviewer, and Dispatcher Agents
Next, build the remaining specialized agents that handle composition, quality assurance, and delivery:
copywriter_agent = autogen.AssistantAgent(
name="CopywriterAgent",
system_message="""You are a B2B sales copywriter specializing in cold outreach emails.
Given a Personalization Brief, compose a complete outreach email that follows these rules:
1. **Subject Line**: Under 60 characters, specific, curiosity-driven, no clickbait.
2. **Opening**: Use the best icebreaker from the Personalization Brief. Be specific.
3. **Body (2-3 paragraphs)**:
- Paragraph 1: Contextualize the trigger and show you did your homework.
- Paragraph 2: Bridge to a relevant value proposition or insight.
- Paragraph 3 (optional): Social proof or a specific, low-commitment ask.
4. **Call-to-Action**: A single, clear next step (e.g., "Would you be open to a 15-minute call next week?")
5. **Closing**: Professional signature block with name, title, company.
Tone: Confident but not pushy. Conversational but polished. Total length: 120-180 words.
Output the complete email with a subject line labeled 'Subject:' and the body below.""",
llm_config=creative_llm_config,
)
reviewer_agent = autogen.AssistantAgent(
name="ReviewerAgent",
system_message="""You are a quality assurance reviewer for sales emails. Your job is to
evaluate the email drafted by the CopywriterAgent against strict criteria:
1. **Accuracy Check**: Are all factual claims supported by the research? Flag any unverified statements.
2. **Personalization Score (1-10)**: Does this feel genuinely tailored or could it be templated?
3. **Tone Check**: Is the tone appropriate—confident, respectful, not desperate?
4. **Spam Risk**: Are there spam-trigger words (guarantee, free, urgent, act now)? List them.
5. **CTA Clarity**: Is the call-to-action specific and easy to act on?
Provide a structured review with:
- Overall verdict: APPROVED or NEEDS_REVISION
- Specific issues (if any) with suggested fixes
- Final score (1-10)
If APPROVED, the email moves to the HumanProxyAgent for final sign-off.
If NEEDS_REVISION, return the feedback to the CopywriterAgent for revision.""",
llm_config=research_llm_config,
)
# The Dispatcher Agent uses tool integration for email sending
def send_outreach_email(to_email: str, subject: str, body: str) -> str:
"""
Send an email via SendGrid (or your preferred email API).
Returns a confirmation with delivery status.
"""
# In production, integrate with SendGrid, Mailgun, or SMTP
import sendgrid
from sendgrid.helpers.mail import Mail, Email, To, Content
sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY"))
message = Mail(
from_email=Email("sales@yourcompany.com", "Your Sales Team"),
to=To(to_email),
subject=subject,
html_content=Content("text/html", body.replace("\n", "
"))
)
try:
response = sg.send(message)
return f"Email sent successfully to {to_email}. Status: {response.status_code}"
except Exception as e:
return f"Failed to send email: {str(e)}"
def log_to_crm(company_name: str, contact_email: str, outcome: str) -> str:
"""Log the outreach activity to CRM for pipeline tracking."""
# Integrate with your actual CRM API here
return f"CRM logged: {company_name} | {contact_email} | {outcome}"
dispatcher_agent = autogen.AssistantAgent(
name="DispatcherAgent",
system_message="""You are the dispatcher. Once the HumanProxyAgent approves an email,
you handle execution:
1. Call send_outreach_email with the recipient email, subject, and body.
2. Call log_to_crm to record the activity.
3. Report back the delivery status and CRM log confirmation.
Only act on emails that have been explicitly APPROVED by the HumanProxyAgent.""",
llm_config=utility_llm_config,
function_map={
"send_outreach_email": send_outreach_email,
"log_to_crm": log_to_crm,
}
)
Step 5: Add the Human Proxy Agent for Oversight
The Human Proxy Agent represents the sales manager's judgment. It pauses execution and waits for actual human input before approving dispatch:
# Human-in-the-loop: UserProxyAgent waits for human input
human_proxy = autogen.UserProxyAgent(
name="HumanProxyAgent",
system_message="""You are the sales manager overseeing the outreach pipeline.
Review the email that has been approved by the ReviewerAgent.
If you approve, type 'APPROVED' and the email will be dispatched.
If you want revisions, provide specific feedback and the CopywriterAgent will revise.
You can also type 'REJECT' to skip this prospect entirely.""",
human_input_mode="ALWAYS", # Waits for real human input
code_execution_config=False,
)
Step 6: Orchestrate with GroupChat and GroupChatManager
Now wire all agents together using AutoGen's group chat mechanism. The GroupChatManager handles turn-taking and message routing based on a defined speaker selection strategy:
# Define the speaker selection logic
def sales_speaker_selection_func(speaker, group_chat):
"""
Custom speaker selection for the sales outreach pipeline.
Ensures the workflow follows: Research → Personalization → Copywriter → Reviewer → Human → Dispatcher
"""
messages = group_chat.messages
# If no messages yet, start with the user (initiator)
if len(messages) <= 1:
return group_chat.agents_by_name["User"]
last_speaker = messages[-1]["name"]
# Define the sequential workflow
workflow_map = {
"User": "ResearchAgent",
"ResearchAgent": "PersonalizationAgent",
"PersonalizationAgent": "CopywriterAgent",
"CopywriterAgent": "ReviewerAgent",
"ReviewerAgent": "HumanProxyAgent",
"HumanProxyAgent": "DispatcherAgent",
"DispatcherAgent": "User", # Loop back for next prospect
}
next_agent_name = workflow_map.get(last_speaker, "User")
return group_chat.agents_by_name.get(next_agent_name, group_chat.agents_by_name["User"])
# Create the group chat
sales_group_chat = autogen.GroupChat(
agents=[
research_agent,
personalization_agent,
copywriter_agent,
reviewer_agent,
human_proxy,
dispatcher_agent,
],
messages=[],
max_round=30,
speaker_selection_method=sales_speaker_selection_func,
allow_repeat_speaker=False,
)
# Create the manager
chat_manager = autogen.GroupChatManager(
groupchat=sales_group_chat,
llm_config=utility_llm_config,
)
Step 7: Launch the Outreach Pipeline
Create the main execution script that feeds prospect data into the agent system. This script iterates through a list of target companies and initiates the conversation for each:
# Main execution: Process a batch of prospects
def run_sales_outreach_pipeline(prospect_list: list[dict]):
"""
prospect_list: List of dicts with keys:
- company_name: str
- contact_name: str
- contact_email: str
- contact_title: str (optional)
"""
results = []
for prospect in prospect_list:
print(f"\n{'='*60}")
print(f"Processing: {prospect['company_name']} - {prospect['contact_name']}")
print(f"{'='*60}")
# The initial message kicks off the workflow
initial_message = f"""
**New Prospect for Outreach:**
Company: {prospect['company_name']}
Contact: {prospect['contact_name']}
Title: {prospect.get('contact_title', 'Unknown')}
Email: {prospect['contact_email']}
Please follow the standard outreach workflow:
1. ResearchAgent: Gather intelligence on {prospect['company_name']}
2. PersonalizationAgent: Create personalization hooks for {prospect['contact_name']}
3. CopywriterAgent: Draft the outreach email
4. ReviewerAgent: Quality-check the email
5. HumanProxyAgent: Await my approval before dispatch
6. DispatcherAgent: Send the approved email to {prospect['contact_email']}
"""
# Initiate the group chat with the User agent (not in our agent list,
# so we add it temporarily or use the manager directly)
user_agent = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config=False,
)
# Add user to the group chat dynamically
sales_group_chat.agents_by_name["User"] = user_agent
# Start the conversation
user_agent.initiate_chat(
recipient=chat_manager,
message=initial_message,
max_turns=25,
)
# Extract the final email from conversation history
results.append({
"company": prospect['company_name'],
"contact": prospect['contact_email'],
"conversation": sales_group_chat.messages[-10:], # Last 10 messages
})
# Clear messages for next prospect (or keep for context if desired)
sales_group_chat.messages.clear()
return results
# Example usage
if __name__ == "__main__":
test_prospects = [
{
"company_name": "Acme Cloud Solutions",
"contact_name": "Sarah Chen",
"contact_email": "sarah.chen@acmecloud.com",
"contact_title": "VP Engineering",
},
{
"company_name": "NexGen Data Systems",
"contact_name": "Marcus Webb",
"contact_email": "marcus.webb@nexgendata.com",
"contact_title": "CTO",
},
]
outreach_results = run_sales_outreach_pipeline(test_prospects)
# Save results for auditing
import json
with open("outreach_results.json", "w") as f:
json.dump(outreach_results, f, indent=2, default=str)
print("\nPipeline complete. Results saved to outreach_results.json")
Step 8: Add Error Handling and Retry Logic
Production systems need robust error handling. Here's how to wrap the pipeline with retry logic and graceful degradation:
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def run_outreach_with_retry(prospect: dict, max_retries: int = 3) -> dict:
"""
Attempt outreach with exponential backoff on failure.
Returns a structured result dict with status and details.
"""
for attempt in range(1, max_retries + 1):
try:
# Re-initialize agents for each attempt to avoid stale state
sales_group_chat.messages.clear()
user_agent = autogen.UserProxyAgent(
name="User",
human_input_mode="NEVER",
code_execution_config=False,
)
sales_group_chat.agents_by_name["User"] = user_agent
initial_message = f"""
**New Prospect for Outreach:**
Company: {prospect['company_name']}
Contact: {prospect['contact_name']}
Email: {prospect['contact_email']}
Title: {prospect.get('contact_title', 'Unknown')}
Please follow the standard outreach workflow.
"""
user_agent.initiate_chat(
recipient=chat_manager,
message=initial_message,
max_turns=25,
)
return {
"status": "success",
"company": prospect['company_name'],
"contact": prospect['contact_email'],
"attempt": attempt,
}
except Exception as e:
logger.error(f"Attempt {attempt} failed for {prospect['company_name']}: {str(e)}")
if attempt < max_retries:
wait_time = 2 ** attempt # Exponential backoff: 2s, 4s, 8s
logger.info(f"Retrying in {wait_time} seconds...")
time.sleep(wait_time)
else:
return {
"status": "failed",
"company": prospect['company_name'],
"contact": prospect['contact_email'],
"error": str(e),
"attempts": max_retries,
}
Best Practices for Sales Outreach Agents
1. Start with a Narrow Scope, Then Expand
Begin by automating a single segment of your outreach (e.g., only warm leads from CRM) before scaling to full cold outreach. This lets you validate the research quality and personalization accuracy in a controlled environment. Many teams find that starting with "re-engagement" campaigns for dormant leads yields quick wins while they fine-tune the system.
2. Implement Progressive Human Oversight
Don't jump straight to fully autonomous dispatch. Use a progressive trust model:
- Phase 1: Human reviews every email before sending (HumanProxyAgent set to
ALWAYS) - Phase 2: Human reviews only emails scored below 8/10 by the ReviewerAgent
- Phase 3: Human reviews a random 10% sample for ongoing calibration
- Phase 4: Full autonomy with weekly retrospective audits
3. Curate Your Research Sources Carefully
The quality of your outreach depends entirely on the quality of your research. Prioritize sources that are:
- Timely: News from the last 30 days, not stale information
- Relevant: Funding rounds, product launches, hiring sprees, executive changes
- Verified: Cross-reference claims across multiple sources when possible
Consider adding a "source credibility" scoring layer that weights information based on the reliability of its origin.
4. Build a Feedback Loop from Outcomes
The system improves when it learns from results. Track these metrics and feed them back into your agent prompts:
- Open rate by personalization hook type: Which triggers generate the most opens?
- Reply rate by CTA style: Do soft CTAs ("open to chatting") outperform hard CTAs ("book a slot")?
- Unsubscribe rate by email length: Is there an optimal word count?
- Meeting conversion rate by research depth: Does deeper research correlate with higher conversions?
5. Use Model Tiering Strategically
Not every step needs GPT-4o. A practical cost optimization strategy:
- Research & Review: Use GPT-4o or Claude 3.5 Sonnet for accuracy and reasoning
- Personalization & Copywriting: Use GPT-4o-mini for creative tasks at lower cost
- Formatting & Dispatch: Use GPT-3.5-turbo or even a rule-based system
This tiering can reduce per-prospect costs by 40-60% without sacrificing quality.
6. Implement Guardrails and Compliance Checks
Sales outreach operates in a regulated environment. Build in these safeguards:
- GDPR/CCPA compliance: Automatically check if the prospect has opted out or requested deletion
- CAN-SPAM adherence: Include unsubscribe links and physical address in every email
- Do-not-contact lists: Cross-reference against internal suppression lists before dispatch
- Rate limiting: Never send more than X emails per hour from a single domain to protect deliverability
7. Version Your Prompts Like Code
Your agent system messages are effectively your "sales playbook encoded in prompts." Treat them as code: version them in Git, A/B test variations, and roll back poorly performing versions. Maintain a changelog that tracks which prompt changes correlated with outcome metric shifts.
Conclusion
Building a Sales Outreach Agent with AutoGen transforms a traditionally manual, labor-intensive process into a scalable, intelligent system that combines the research depth of a diligent analyst, the creative flair of a skilled copywriter, and the consistency of a rigorous QA process—all orchestrated through collaborative multi-agent conversation. The architecture presented here gives you a production-ready foundation: specialized agents for research, personalization, composition, review, and dispatch, coordinated through AutoGen's GroupChat with human-in-the-loop oversight.
The key insight is that this isn't about replacing sales professionals—it's about amplifying them. By offloading repetitive research and drafting work to AI agents, your human team can focus on strategic relationship-building, nuanced negotiation, and the high-touch moments that actually close deals. Start with the narrow-scope implementation described in the best practices, iterate based on outcome data, and progressively expand autonomy as your confidence in the system grows. The result is a sales outreach engine that operates with the precision of software and the personal touch of thoughtful human oversight.