Pedro Alonso

Building Your AI-Powered Sales Engine Part 2: From Lead Generation to Client Acquisition

11 min read

1. The Meta-Solution: Automating Your Own Sales

In Part 1, we covered practical agent ideas and the 4-week launch plan. You built an agent for yourself, identified a niche, and maybe even sent your first few outreach emails.

But here’s the bottleneck: Finding and reaching out to 100+ qualified prospects manually is exhausting.

You’re a developer. You automate things. Why are you manually:

  • Searching Google Maps for businesses?
  • Visiting websites one by one?
  • Writing personalized emails by hand?
  • Tracking who responded in a spreadsheet?

The irony: You’re selling automation while doing manual labor.

The solution: Build AI agents to automate your own client acquisition.

In this part, we’ll build three connected agents:

  • Lead Generation Agent — Sources 100+ prospects in your niche, visits their sites, detects real automation gaps (forms, follow-ups, integrations), and ranks them 0–10 on likelihood to buy.
  • Cold Email Agent — Turns those gaps into short, specific emails that read like you wrote them after browsing their site. No templates, each draft anchored to something they actually do.
  • Proposal Generator Agent — When someone bites, it converts your conversation into a clear, priced proposal in minutes—deliverables, timeline, and next steps included.

What you’ll walk away with today:

  • A CSV of ranked leads you can sort and filter
  • A folder of ready-to-send, personalized emails for the top 20
  • Proposal drafts you can tweak and send the same day

Let’s build it.

2. The Lead Generation Agent: Finding 100 Qualified Prospects in an Hour

2.1 What We’re Building

Input:

  • Niche: “real estate agents”
  • Location: “Austin, Texas”
  • Automation type: “lead capture”

Output:

  • 100 businesses with contact info
  • Automation opportunity identified for each
  • Personalized outreach angle
  • Scored by likelihood to convert

The Process:

  1. Search Google Maps for businesses
  2. Extract website URLs
  3. Scrape each website for signals
  4. Use LLM to identify automation opportunities
  5. Score and rank leads
  6. Generate personalized outreach angles

2.2 Architecture Overview

Here’s how the Lead Generation Agent flows through its decision-making process:

Raw results
Structured business data
Website HTML
Content + forms + integrations
Automation signals detected
Ranked opportunities
Personalized email drafts
SearchGoogleMaps
ParseBusinessInfo
FetchWebsites
AnalyzeContent
IdentifyOpportunities
ScoreLeads
GenerateOutreach
Tavily API: Google Maps search
Returns: name, website, phone, location
LLM analyzes website for:
- Forms (lead capture)
- Manual processes
- Integration opportunities
Scoring criteria:
- Website quality (1-3)
- Automation signals (0-5)
- Business size (1-2)
Total: 0-10

Lead Generation Agent state machine workflow

Code implementation:

from typing import TypedDict, List, Dict, Optional
from langgraph.graph import StateGraph, END
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_tavily import TavilySearch
from pydantic import BaseModel, Field
import requests
from bs4 import BeautifulSoup
import json
class Business(BaseModel):
"""Individual business data"""
name: str
website: Optional[str]
phone: Optional[str]
email: Optional[str]
location: str
class AutomationOpportunity(BaseModel):
"""Identified automation opportunity"""
business_name: str
opportunity_type: str
pain_point: str
solution_angle: str
estimated_value: str
score: int = Field(ge=0, le=10)
class LeadGenState(TypedDict):
"""State for lead generation agent"""
niche: str
location: str
automation_type: str
# Search results
raw_search_results: List[Dict]
businesses: List[Business]
# Website analysis
website_content: Dict[str, str]
automation_signals: Dict[str, List[str]]
# Scoring
opportunities: List[AutomationOpportunity]
scored_leads: List[Dict]
# Output
outreach_templates: Dict[str, str]

2.3 Node 1: Google Maps Search with Tavily

def search_google_maps_node(state: LeadGenState):
"""
Use Tavily to search Google Maps and extract business info.
"""
print(f"Searching for {state['niche']} in {state['location']}...")
tavily = TavilySearch(
max_results=20,
api_key=os.getenv("TAVILY_API_KEY")
)
# Construct search query optimized for Google Maps
query = f"{state['niche']} in {state['location']} site:google.com/maps"
# Also search for business directories
queries = [
f"{state['niche']} in {state['location']} site:google.com/maps",
f"{state['niche']} {state['location']} contact",
f"top {state['niche']} {state['location']}"
]
all_results = []
for q in queries:
try:
results = tavily.invoke(q)
all_results.extend(results if isinstance(results, list) else [results])
except Exception as e:
print(f"⚠️ Search failed for '{q}': {e}")
print(f"Found {len(all_results)} raw results")
return {
"raw_search_results": all_results
}
def extract_business_info_node(state: LeadGenState):
"""
Parse search results and extract business information.
Use LLM to help extract structured data.
"""
print("Extracting business information...")
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0
)
businesses = []
for result in state['raw_search_results'][:50]: # Limit to 50 for cost
# Try to extract website from result
content = result.get('content', '')
url = result.get('url', '')
# Use LLM to extract structured business info
prompt = f"""Extract business information from this search result:
URL: {url}
Content: {content}
Extract and return JSON with:
- name: business name
- website: their website URL (not Google Maps link)
- phone: phone number if found
- email: email if found
- location: city/area
If you can't find something, use null. Return only valid JSON.
"""
try:
response = llm.invoke(prompt).content
# Parse JSON from response
business_data = json.loads(response)
if business_data.get('website'):
businesses.append(Business(**business_data))
except Exception as e:
print(f"⚠️ Failed to parse: {e}")
continue
print(f"Extracted {len(businesses)} businesses with websites")
return {"businesses": businesses}

2.4 Node 2: Website Content Extraction

class WebsiteScraper:
"""Extract and analyze website content."""
@staticmethod
def extract_content(url: str) -> str:
"""Scrape main content from a URL."""
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
# Remove unwanted elements
for element in soup(['script', 'style', 'nav', 'footer', 'header']):
element.decompose()
# Extract text
text = soup.get_text(separator='\n', strip=True)
# Clean up
lines = [line.strip() for line in text.splitlines() if line.strip()]
content = '\n'.join(lines)
return content[:8000] # Limit for LLM context
except Exception as e:
print(f"❌ Error scraping {url}: {e}")
return ""
@staticmethod
def extract_forms(url: str) -> List[Dict]:
"""Identify forms on the website."""
try:
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.content, 'html.parser')
forms = []
for form in soup.find_all('form'):
form_data = {
'action': form.get('action', ''),
'method': form.get('method', 'get'),
'inputs': len(form.find_all(['input', 'textarea', 'select']))
}
forms.append(form_data)
return forms
except Exception as e:
return []
def scrape_websites_node(state: LeadGenState):
"""
Scrape each business website for content and signals.
"""
print("Scraping websites for automation signals...")
scraper = WebsiteScraper()
website_content = {}
automation_signals = {}
for business in state['businesses'][:30]: # Limit for API costs
if not business.website:
continue
print(f" Scraping {business.name}...")
# Get main content
content = scraper.extract_content(business.website)
website_content[business.name] = content
# Look for automation signals
forms = scraper.extract_forms(business.website)
signals = []
if forms:
signals.append(f"Has {len(forms)} form(s) - potential for form automation")
if 'contact' in content.lower():
signals.append("Has contact form - lead capture opportunity")
if 'booking' in content.lower() or 'schedule' in content.lower():
signals.append("Scheduling system - calendar automation opportunity")
if 'newsletter' in content.lower() or 'subscribe' in content.lower():
signals.append("Newsletter signup - email automation opportunity")
automation_signals[business.name] = signals
print(f"Scraped {len(website_content)} websites")
return {
"website_content": website_content,
"automation_signals": automation_signals
}

2.5 Node 3: Opportunity Identification with LLM

def identify_opportunities_node(state: LeadGenState):
"""
Use LLM to analyze websites and identify specific automation opportunities.
"""
print("Identifying automation opportunities...")
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0.3
)
structured_llm = llm.with_structured_output(AutomationOpportunity)
opportunities = []
for business in state['businesses']:
if business.name not in state['website_content']:
continue
content = state['website_content'][business.name]
signals = state['automation_signals'].get(business.name, [])
prompt = f"""You are an automation consultant analyzing a business website.
Business: {business.name}
Type: {state['niche']}
Website Content (excerpt):
{content[:2000]}
Automation Signals Found:
{', '.join(signals) if signals else 'None detected'}
Looking for: {state['automation_type']} automation opportunities
Analyze this business and identify:
1. opportunity_type: Specific type of automation they need
2. pain_point: What manual process is causing them pain
3. solution_angle: How your automation solves it (be specific!)
4. estimated_value: How much time/money this saves them
5. score: Rate 1-10 how likely they are to buy (based on pain point severity)
Be specific and actionable. Reference actual content from their site.
"""
try:
opportunity = structured_llm.invoke(prompt)
opportunities.append(opportunity)
print(f" {business.name}: {opportunity.opportunity_type} (score: {opportunity.score})")
except Exception as e:
print(f" ⚠️ Failed for {business.name}: {e}")
continue
# Sort by score
opportunities.sort(key=lambda x: x.score, reverse=True)
print(f"Identified {len(opportunities)} opportunities")
return {"opportunities": opportunities}
def score_and_rank_node(state: LeadGenState):
"""
Create final scored lead list with all information.
"""
print("Scoring and ranking leads...")
scored_leads = []
for opp in state['opportunities']:
# Find the matching business
business = next(
(b for b in state['businesses'] if b.name == opp.business_name),
None
)
if business:
lead = {
'business_name': business.name,
'website': business.website,
'email': business.email,
'phone': business.phone,
'location': business.location,
'opportunity_type': opp.opportunity_type,
'pain_point': opp.pain_point,
'solution_angle': opp.solution_angle,
'estimated_value': opp.estimated_value,
'score': opp.score
}
scored_leads.append(lead)
print(f"Created {len(scored_leads)} scored leads")
return {"scored_leads": scored_leads}

2.6 Node 4: Personalized Outreach Generation

def generate_outreach_node(state: LeadGenState):
"""
Generate personalized cold email for each lead.
NOT a template - each email is unique based on their website.
"""
print("Generating personalized outreach emails...")
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
temperature=0.7
)
outreach_templates = {}
# Only generate for top 20 leads
for lead in state['scored_leads'][:20]:
prompt = f"""Write a personalized cold email for this lead:
Business: {lead['business_name']}
Type: {state['niche']}
Website: {lead['website']}
Pain Point Identified: {lead['pain_point']}
Solution: {lead['solution_angle']}
Value: {lead['estimated_value']}
Write a short, personalized cold email (max 150 words) that:
1. References something SPECIFIC from their website
2. Identifies their pain point (without being presumptuous)
3. Offers to show them a solution (not sell)
4. Includes a soft CTA to watch a demo video
5. Feels like a human wrote it, not a template
Subject line should be specific to their business, not generic.
Format:
Subject: [subject]
[email body]
Do NOT use: "I hope this email finds you well" or similar clichés.
Do NOT use: "I noticed you're looking to..." unless they literally say that.
DO reference actual content from their website.
"""
try:
email = llm.invoke(prompt).content
outreach_templates[lead['business_name']] = email
except Exception as e:
print(f" ⚠️ Failed for {lead['business_name']}: {e}")
print(f"Generated {len(outreach_templates)} personalized emails")
return {"outreach_templates": outreach_templates}

2.7 Building the Complete Graph

def create_lead_generation_agent():
"""
Build and compile the lead generation agent.
"""
workflow = StateGraph(LeadGenState)
# Add all nodes
workflow.add_node("searcher", search_google_maps_node)
workflow.add_node("extractor", extract_business_info_node)
workflow.add_node("scraper", scrape_websites_node)
workflow.add_node("analyzer", identify_opportunities_node)
workflow.add_node("scorer", score_and_rank_node)
workflow.add_node("writer", generate_outreach_node)
# Connect the flow
workflow.set_entry_point("searcher")
workflow.add_edge("searcher", "extractor")
workflow.add_edge("extractor", "scraper")
workflow.add_edge("scraper", "analyzer")
workflow.add_edge("analyzer", "scorer")
workflow.add_edge("scorer", "writer")
workflow.add_edge("writer", END)
return workflow.compile()
# Usage
if __name__ == "__main__":
agent = create_lead_generation_agent()
result = agent.invoke({
"niche": "real estate agents",
"location": "Austin, Texas",
"automation_type": "lead capture",
"raw_search_results": [],
"businesses": [],
"website_content": {},
"automation_signals": {},
"opportunities": [],
"scored_leads": [],
"outreach_templates": {}
})
# Export results
import csv
with open('leads.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=result['scored_leads'][0].keys())
writer.writeheader()
writer.writerows(result['scored_leads'])
# Save email templates
with open('outreach_emails.json', 'w') as f:
json.dump(result['outreach_templates'], f, indent=2)
print("\n✅ Lead generation complete!")
print(f"📊 {len(result['scored_leads'])} leads saved to leads.csv")
print(f"✉️ {len(result['outreach_templates'])} emails saved to outreach_emails.json")

2.8 Output Example

leads.csv:

business_name,website,email,score,opportunity_type,pain_point,estimated_value
"Austin Home Realty",austinrealty.com,info@austin...,9,"Lead Capture Automation","Manual CRM entry from website forms","Saves 8 hours/week = $1,200/month"
"Sarah Chen Properties",sarahchen.com,contact@sarah...,8,"Follow-up Sequence","No automated follow-up for leads","Increases conversion by 30%"

outreach_emails.json:

{
"Austin Home Realty": "Subject: Quick idea for your Zillow lead forms\n\nHi,\n\nI was looking at austinrealty.com and noticed you have lead forms for property searches and seller consultations.\n\nI'm guessing when someone fills these out, you're manually copying that info into your CRM and then sending them a follow-up email?\n\nI built a simple automation that:\n- Captures form submissions automatically\n- Adds them to your CRM with tags\n- Sends personalized follow-up based on their interest\n- Creates a reminder for you to call\n\nHere's a 2-minute video showing how it works: [demo link]\n\nIf it looks useful for Austin Home Realty, I can set it up.\n\n- Pedro\n\nP.S. Love that you specialize in East Austin - that market's been crazy lately."
}

3. The Cold Email Agent: Automated Follow-ups

3.1 Beyond Templates: Truly Personalized Follow-ups

The outreach agent generated initial emails. Now we need follow-ups.

The problem with traditional follow-ups:

  • “Just following up on my previous email”
  • Generic and obvious
  • Decreasing response rates

The AI agent approach:

  • Read their response (or lack of)
  • Check if they’ve visited your demo link
  • Find new information about their business
  • Generate contextual follow-up

3.2 Follow-up Agent Architecture

class FollowUpState(TypedDict):
original_email: str
days_since_sent: int
link_clicked: bool
business_name: str
business_website: str
recent_activity: Optional[str] # New info about their business
follow_up_email: str
def check_engagement_node(state: FollowUpState):
"""
Check if they engaged with your email.
(Integrate with email tracking service)
"""
# In production, integrate with:
# - SendGrid/Mailgun tracking
# - Link shortener analytics
# - Website visitor tracking
engagement = {
'opened': False,
'clicked': state['link_clicked'],
'replied': False
}
return {"engagement": engagement}
def research_business_updates_node(state: FollowUpState):
"""
Check for recent news/updates about the business.
"""
tavily = TavilySearch(max_results=3)
# Search for recent activity
query = f"{state['business_name']} recent news OR updates"
try:
results = tavily.invoke(query)
recent_info = results[0]['content'] if results else None
return {"recent_activity": recent_info}
except:
return {"recent_activity": None}
def generate_contextual_followup_node(state: FollowUpState):
"""
Generate follow-up based on engagement and new info.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0.7)
context = f"""
Original email sent {state['days_since_sent']} days ago:
{state['original_email']}
Engagement:
- Link clicked: {state['link_clicked']}
Recent business activity:
{state['recent_activity'] or 'None found'}
"""
prompt = f"""Write a brief follow-up email (max 100 words).
{context}
Rules:
- If they clicked the link: Reference that they checked it out, ask if they have questions
- If recent activity: Reference it naturally (new listing, event, etc.)
- If no engagement: Add new value (case study, different angle)
- NEVER say "just following up" or "circling back"
- Keep it conversational and brief
Subject: [subject]
[body]
"""
follow_up = llm.invoke(prompt).content
return {"follow_up_email": follow_up}

3.3 The 3-Touch Follow-up Sequence

def create_follow_up_sequence(lead: Dict, original_email: str):
"""
Automated 3-touch follow-up sequence.
"""
# Day 3: Value-add follow-up
day_3_prompt = f"""
They haven't responded to your original email about {lead['opportunity_type']}.
Write a follow-up that adds NEW value:
- Share a quick tip related to their pain point
- Link to a case study or example
- Offer a different angle
Max 80 words. No "just checking in" language.
"""
# Day 7: Social proof follow-up
day_7_prompt = f"""
Still no response. Write a follow-up that:
- Mentions you set this up for another {lead['niche']} business
- Shares a specific result
- Soft CTA: "Want to see how it works?"
Max 80 words.
"""
# Day 14: Breakup email
day_14_prompt = f"""
Final follow-up. Write a "breakup" email:
- Acknowledge they're probably not interested
- Offer one last resource (free guide, checklist)
- Make it easy to say no
- Leave door open for future
Max 100 words. Friendly and professional.
"""
# Generate each follow-up
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
sequence = {
'day_3': llm.invoke(day_3_prompt).content,
'day_7': llm.invoke(day_7_prompt).content,
'day_14': llm.invoke(day_14_prompt).content
}
return sequence

4. The Proposal Generator Agent

4.1 From Conversation to Professional Quote

When a lead responds positively, you need to send a proposal quickly.

Manual way: Spend 2 hours writing a custom proposal Agent way: Generate it in 2 minutes

4.2 Proposal Agent Implementation

class ProposalState(TypedDict):
business_name: str
contact_name: str
opportunity_type: str
conversation_history: str
custom_requirements: List[str]
pricing_tier: str
proposal_document: str
def analyze_requirements_node(state: ProposalState):
"""
Extract specific requirements from conversation.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
prompt = f"""Analyze this conversation and extract specific requirements:
{state['conversation_history']}
List all:
- Technical requirements (integrations, tools, features)
- Business requirements (timeline, budget constraints)
- Success criteria (what does done look like?)
Return as JSON array of requirements.
"""
response = llm.invoke(prompt).content
requirements = json.loads(response)
return {"custom_requirements": requirements}
def determine_pricing_node(state: ProposalState):
"""
Determine appropriate pricing tier based on complexity.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
prompt = f"""Based on these requirements, recommend pricing tier:
Requirements:
{json.dumps(state['custom_requirements'], indent=2)}
Opportunity type: {state['opportunity_type']}
Pricing tiers:
- Simple: $1,500 setup + $150/month (basic form automation, single integration)
- Standard: $3,500 setup + $300/month (AI features, multiple integrations)
- Premium: $5,000+ setup + $500/month (custom AI agents, complex workflows)
Return JSON:
{{
"tier": "simple|standard|premium",
"setup_price": number,
"monthly_price": number,
"justification": "why this tier fits"
}}
"""
response = llm.invoke(prompt).content
pricing = json.loads(response)
return {"pricing_tier": pricing}
def generate_proposal_node(state: ProposalState):
"""
Generate complete proposal document.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0.5)
pricing = state['pricing_tier']
prompt = f"""Create a professional proposal document in Markdown.
For: {state['business_name']}
Contact: {state['contact_name']}
Project: {state['opportunity_type']}
Requirements:
{json.dumps(state['custom_requirements'], indent=2)}
Pricing:
- Setup: ${pricing['setup_price']}
- Monthly: ${pricing['monthly_price']}
- Justification: {pricing['justification']}
Include:
1. Executive Summary (what we're solving)
2. Proposed Solution (technical overview in plain language)
3. Deliverables (specific items they'll receive)
4. Timeline (realistic phases)
5. Pricing (breakdown)
6. Next Steps (how to move forward)
Professional but conversational tone. Use their business name throughout.
Make it feel custom, not templated.
"""
proposal = llm.invoke(prompt).content
# Add header
full_proposal = f"""# Proposal for {state['business_name']}
**Prepared for:** {state['contact_name']}
**Date:** {datetime.now().strftime('%B %d, %Y')}
**Project:** {state['opportunity_type']}
---
{proposal}
---
**Ready to get started?** Reply to this email or schedule a call: [calendar link]
Best regards,
[Your Name]
"""
return {"proposal_document": full_proposal}
# Build the graph
def create_proposal_agent():
workflow = StateGraph(ProposalState)
workflow.add_node("analyzer", analyze_requirements_node)
workflow.add_node("pricer", determine_pricing_node)
workflow.add_node("generator", generate_proposal_node)
workflow.set_entry_point("analyzer")
workflow.add_edge("analyzer", "pricer")
workflow.add_edge("pricer", "generator")
workflow.add_edge("generator", END)
return workflow.compile()

4.3 Usage Example

# When a lead responds positively
conversation = """
Lead: "Hi, I watched your demo. This could be useful. We get about
20 form submissions daily and manually enter them into Salesforce.
Takes about 30 minutes. Can you also send them a welcome email
automatically?"
You: "Absolutely. The automation would capture submissions in real-time,
add them to Salesforce with proper tags, and send personalized welcome
emails. Would you also want follow-up sequences based on their interests?"
Lead: "Yes, that would be great. How much does something like this cost?
And how long to set up?"
"""
proposal_agent = create_proposal_agent()
result = proposal_agent.invoke({
"business_name": "Austin Home Realty",
"contact_name": "Mike",
"opportunity_type": "Lead Capture Automation",
"conversation_history": conversation,
"custom_requirements": [],
"pricing_tier": {},
"proposal_document": ""
})
# Save proposal
with open(f"proposal_{business_name}.md", 'w') as f:
f.write(result['proposal_document'])
# Also export as PDF (using markdown-pdf or similar)
print("✅ Proposal generated and saved!")

5. Connecting the System: The Complete Workflow

Important: The code below shows how all the agents connect together conceptually. However, don’t run this as fully automated. Section 7 shows the safe “AI Co-pilot” approach where the system generates drafts and you review before sending. The code here is for understanding the data flow between agents.

5.1 System Architecture

Here’s how the three agents work together in your sales pipeline:

📊 Output
📄 Agent 3: Proposal Generator
✉️ Agent 2: Cold Email
🔍 Agent 1: Lead Generation
📥 Input
Top 20 leads
Positive response
100 scored leads
Personalized emails
Proposals generated
Meetings booked
Parse Conversation
Calculate Pricing
Generate Proposal
Export PDF
Generate Email 1
Track Opens/Clicks
Follow-up Logic Day 3,7,14
Response Detection
Google Maps Search
Website Scraping
LLM Analysis
Score & Rank 0-10
Niche: real estate agents
Location: Austin TX
Automation: lead capture

Complete AI Sales Pipeline architecture showing all three agents

Data flow:

  1. Lead Gen Agent → Outputs scored leads with automation opportunities
  2. Cold Email Agent → Takes top leads, sends personalized outreach, tracks engagement
  3. Proposal Agent → Triggered by positive responses, generates professional quotes
  4. You → Review drafts, approve sends, close deals (Section 7)

5.2 The Full Pipeline Code

class SalesAutomationState(TypedDict):
# Lead gen
niche: str
location: str
leads: List[Dict]
# Outreach
emails_sent: List[Dict]
responses: List[Dict]
# Follow-up
follow_ups_scheduled: List[Dict]
# Proposals
proposals_generated: List[Dict]
def orchestrate_sales_pipeline():
"""Coordinate sub-agents."""
# 1. Generate leads
lead_agent = create_lead_generation_agent()
lead_results = lead_agent.invoke({
"niche": "real estate agents",
"location": "Austin, Texas",
"automation_type": "lead capture",
# ... other fields
})
print(f"✅ Generated {len(lead_results['scored_leads'])} leads")
# 2. Send initial outreach (top 20 leads)
emails_sent = []
for lead in lead_results['scored_leads'][:20]:
email = lead_results['outreach_templates'][lead['business_name']]
# Send email (integrate with SendGrid/Mailgun)
send_email(
to=lead['email'],
subject=email.split('\n')[0].replace('Subject: ', ''),
body='\n'.join(email.split('\n')[1:]),
track_opens=True,
track_clicks=True
)
emails_sent.append({
'business': lead['business_name'],
'sent_date': datetime.now(),
'email_id': email_id
})
print(f"✉️ Sent {len(emails_sent)} initial emails")
# 3. Schedule follow-ups (automated)
for sent_email in emails_sent:
schedule_follow_up(
business=sent_email['business'],
original_email=sent_email,
days=[3, 7, 14]
)
# 4. Monitor responses
# (This runs continuously as a separate process)
def monitor_responses():
while True:
# Check for new replies
responses = check_email_responses()
for response in responses:
if is_positive_response(response):
# Generate proposal
proposal_agent = create_proposal_agent()
proposal = proposal_agent.invoke({
"business_name": response['business_name'],
"contact_name": response['contact_name'],
"conversation_history": response['thread'],
# ...
})
# Send proposal
send_email(
to=response['email'],
subject=f"Proposal for {response['business_name']}",
body=proposal['proposal_document']
)
print(f"📄 Sent proposal to {response['business_name']}")
time.sleep(3600) # Check every hour
def is_positive_response(response: Dict) -> bool:
"""
Use LLM to determine if response indicates interest.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
prompt = f"""Is this email response positive/interested?
Email: {response['body']}
Return JSON: {{"interested": true/false, "confidence": 0-10}}
"""
result = json.loads(llm.invoke(prompt).content)
return result['interested'] and result['confidence'] >= 6

5.2 Integration with Email Services

import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, TrackingSettings, ClickTracking, OpenTracking
def send_email(to: str, subject: str, body: str, track_opens=True, track_clicks=True):
"""
Send email via SendGrid with tracking.
"""
message = Mail(
from_email=os.getenv('FROM_EMAIL'),
to_emails=to,
subject=subject,
html_content=body
)
# Enable tracking
message.tracking_settings = TrackingSettings()
message.tracking_settings.click_tracking = ClickTracking(True, True)
message.tracking_settings.open_tracking = OpenTracking(True)
try:
sg = SendGridAPIClient(os.getenv('SENDGRID_API_KEY'))
response = sg.send(message)
return {
'success': True,
'email_id': response.headers.get('X-Message-Id'),
'status_code': response.status_code
}
except Exception as e:
print(f"❌ Failed to send email: {e}")
return {'success': False, 'error': str(e)}
# Note: In production, add basic retry with backoff and respect provider rate limits.
def check_email_responses():
"""
Check for email responses (integrate with your email provider's API).
"""
# For Gmail API:
from googleapiclient.discovery import build
service = build('gmail', 'v1', credentials=creds)
results = service.users().messages().list(
userId='me',
q='is:unread category:primary'
).execute()
messages = results.get('messages', [])
responses = []
for message in messages:
# Parse message, extract thread, etc.
pass
return responses

6. Advanced Strategies & Optimizations

6.1 Cost Management

Problem: Running LLM calls for 100 leads gets expensive

Solutions:

# 1. Batch processing
def batch_analyze_opportunities(businesses: List[Business], batch_size=10):
"""
Analyze multiple businesses in one LLM call.
"""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
batches = [businesses[i:i+batch_size] for i in range(0, len(businesses), batch_size)]
all_opportunities = []
for batch in batches:
# Single prompt for multiple businesses
prompt = f"""Analyze these {len(batch)} businesses for automation opportunities:
{json.dumps([{
'name': b.name,
'website_content': website_content[b.name][:500] # Limited content
} for b in batch], indent=2)}
For each business, return automation opportunity and score.
"""
response = llm.invoke(prompt)
# Parse response...
return all_opportunities
# 2. Caching
from functools import lru_cache
@lru_cache(maxsize=100)
def analyze_website_cached(url: str):
"""Cache website analysis results."""
return analyze_website(url)
# 3. Use cheaper models for simple tasks
def simple_extraction(content: str):
"""Use Gemini Flash for simple extraction."""
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash") # Cheaper
return llm.invoke(content)
def complex_analysis(content: str):
"""Use GPT-4 only for complex analysis."""
llm = ChatOpenAI(model="gpt-4") # More expensive
return llm.invoke(content)

6.2 Quality Filtering

Problem: Not all leads from Google Maps are good

Solution: Multi-stage filtering

def quality_filter_pipeline(raw_leads: List[Dict]) -> List[Dict]:
"""
Filter leads through multiple quality gates.
"""
# Stage 1: Must have website
with_websites = [l for l in raw_leads if l.get('website')]
print(f"After website filter: {len(with_websites)}")
# Stage 2: Website must load
loading_sites = []
for lead in with_websites:
try:
requests.head(lead['website'], timeout=5)
loading_sites.append(lead)
except:
pass
print(f"After load filter: {len(loading_sites)}")
# Stage 3: Must have automation signals
with_signals = [
l for l in loading_sites
if len(automation_signals.get(l['name'], [])) > 0
]
print(f"After signal filter: {len(with_signals)}")
# Stage 4: LLM quality score > 6
high_quality = [
l for l in with_signals
if opportunities.get(l['name'], {}).get('score', 0) >= 6
]
print(f"Final quality leads: {len(high_quality)}")
return high_quality

6.3 Response Rate Optimization

Track what works:

class EmailPerformance:
def __init__(self):
self.db = {} # In production: use actual database
def track_email(self, email_id: str, metadata: Dict):
"""Track email metadata for A/B testing."""
self.db[email_id] = {
'subject_type': metadata['subject_type'], # question, value-prop, etc.
'length': len(metadata['body']),
'personalization_level': metadata['personalization'],
'sent_time': datetime.now(),
'opened': False,
'clicked': False,
'replied': False
}
def update_engagement(self, email_id: str, event: str):
"""Update when engagement events occur."""
if email_id in self.db:
self.db[email_id][event] = True
def get_best_performing_pattern(self) -> Dict:
"""Analyze what patterns get best responses."""
# Group by subject_type
patterns = {}
for email_id, data in self.db.items():
subject_type = data['subject_type']
if subject_type not in patterns:
patterns[subject_type] = {'sent': 0, 'replied': 0}
patterns[subject_type]['sent'] += 1
if data['replied']:
patterns[subject_type]['replied'] += 1
# Calculate response rates
for pattern in patterns.values():
pattern['response_rate'] = pattern['replied'] / pattern['sent']
# Return best performing
best = max(patterns.items(), key=lambda x: x[1]['response_rate'])
return {
'pattern': best[0],
'response_rate': best[1]['response_rate']
}
# Usage
performance = EmailPerformance()
# When sending
performance.track_email(email_id, {
'subject_type': 'question', # "Quick question about..." vs "Idea for..."
'body': email_body,
'personalization': 'high' # based on website-specific references
})
# When checking results
best = performance.get_best_performing_pattern()
print(f"Use '{best['pattern']}' subjects - {best['response_rate']*100}% response rate")

7. The Complete System: Putting It All Together

7.1 Your AI Sales Team

You now have four specialized agents:

  1. Lead Generator: Finds 100 qualified prospects in 1 hour
  2. Outreach Writer: Creates personalized emails at scale
  3. Follow-up Manager: Nurtures leads automatically
  4. Proposal Creator: Turns conversations into quotes

7.2 Weekly Workflow (With Human Review Gates)

The Critical Rule: The AI generates, you approve. Never let the system send emails unsupervised.

Here’s the weekly workflow with proper human oversight:

AI AgentsDraft FolderYou (Human)Sent EmailsProspectsMonday: Lead GenerationTuesday: Draft CreationWednesday-Friday: Monitor & RespondSaturday: Follow-upsTotal Time: 5-10 hrs/week (safe approach)Search Google Maps (100 leads)Analyze websitesScore opportunities (0-10)Save top 50 leads to CSVReview leads (30 mins)Select top 20 to contactGenerate personalized emailsSave 20 draft emailsReview drafts (30 mins)Edit/approve/reject eachSend approved emailsResponses come inAnalyze response sentimentGenerate reply draftsReview replies (20 mins/day)Send approved repliesCheck who didn't respondGenerate follow-up draftsQuick review (30 mins)Send follow-upsAI AgentsDraft FolderYou (Human)Sent EmailsProspects

Weekly AI Co-pilot workflow with human review gates

Time breakdown:

  • Manual (no AI): 40+ hours/week to manage 20 prospects
  • Fully automated (risky): 2 hours/week, but you’ll burn your reputation
  • AI Co-pilot (smart): 5-10 hours/week, high quality, safe

Monday: Generate leads

# Run lead generation agent
leads = lead_gen_agent.invoke({
"niche": "your_niche",
"location": "target_city",
"automation_type": "your_service"
})
# Export to CRM or CSV for review
export_leads(leads['scored_leads'][:50])

Tuesday: Review & approve outreach

# AI generates drafts, saves to review folder
for lead in leads['scored_leads'][:20]:
draft = outreach_templates[lead['business_name']]
# Save to drafts folder (NOT sent yet)
save_draft(f"drafts/{lead['business_name']}.txt", draft)
# YOU review the drafts
print("📋 Review 20 drafts in ~/drafts/")
print("Your job: Approve, tweak, or reject in 30 minutes")
print("The AI did 90% of the work. You do the final 10% quality check.")
# After review, send approved emails
approved_emails = review_and_approve_drafts("drafts/")
send_batch_emails(approved_emails)

Why this matters:

  • One bad email can damage your reputation
  • Personalization might be off (LLM hallucinated a detail)
  • Some leads might be totally wrong despite high scores
  • You’re the final quality gate, not the bottleneck
  • Blockers: wrong lead, shaky personalization → Fix: 10‑second skim before send

Wednesday-Friday: Monitor responses

# Automated response monitoring (but YOU handle replies)
responses = check_responses()
for response in responses:
if is_positive(response):
# AI generates proposal draft
proposal_draft = proposal_agent.invoke(response)
# Save for your review
save_for_review(f"proposals/{response['business_name']}.md", proposal_draft)
print(f"📄 Review proposal draft for {response['business_name']}")
elif is_question(response):
# AI suggests answer, you approve/edit
suggested_answer = answer_agent.invoke(response)
print(f"💬 Review suggested answer for {response['business_name']}")
# You send after reviewing
send_reviewed_proposals()
send_reviewed_answers()

The AI Co-pilot Principle:

  • AI does: Research, drafting, formatting, tracking
  • You do: Final review, approval, quality control, actual sending
  • Result: 20x faster than manual, but you maintain quality and reputation

Saturday: Follow-ups (automated generation, manual review)

# System generates follow-up drafts (doesn't auto-send)
follow_up_drafts = generate_scheduled_followups()
# You review the queue (takes 15 minutes)
review_and_approve_followups(follow_up_drafts)

Time breakdown:

  • Manual approach: 40 hours/week
  • Fully automated (risky): 2 hours/week, but you’ll burn your reputation
  • AI Co-pilot (smart): 5-10 hours/week, maintain quality, scale safely

7.3 Sample Implementation (AI Co-pilot Pattern)

Here’s the complete implementation with human review gates:

"""
AI Sales Co-pilot System
Generates leads, drafts outreach, manages follow-ups
IMPORTANT: Humans review before sending
"""
import os
from datetime import datetime
from typing import List, Dict
from pathlib import Path
class AISalesCopilot:
"""
AI assistant for sales - generates drafts, you approve.
NOT a fully autonomous spam cannon.
"""
def __init__(self):
self.lead_gen_agent = create_lead_generation_agent()
self.proposal_agent = create_proposal_agent()
self.performance = EmailPerformance()
# Create review folders
Path("drafts/outreach").mkdir(parents=True, exist_ok=True)
Path("drafts/proposals").mkdir(parents=True, exist_ok=True)
Path("drafts/followups").mkdir(parents=True, exist_ok=True)
def generate_outreach_drafts(self, niche: str, location: str):
"""Generate lead list and email drafts for review."""
print(f"🔄 Generating leads and drafts: {niche} in {location}")
result = self.lead_gen_agent.invoke({
"niche": niche,
"location": location,
"automation_type": "lead capture",
})
# Save leads for review
self.save_leads_csv(result['scored_leads'])
# Save email drafts (NOT sent)
drafts_generated = 0
for lead in result['scored_leads'][:20]:
draft = result['outreach_templates'].get(lead['business_name'])
if draft:
self.save_draft(
f"drafts/outreach/{lead['business_name']}.txt",
draft,
lead
)
drafts_generated += 1
print(f"\n✅ Generated {drafts_generated} drafts")
print(f"📋 Review them in: drafts/outreach/")
print(f"⏱️ This should take you ~20-30 minutes to review")
return result
def save_draft(self, filepath: str, content: str, metadata: Dict):
"""Save draft with metadata for review."""
with open(filepath, 'w') as f:
f.write(f"# DRAFT - NOT SENT\n")
f.write(f"# Business: {metadata['business_name']}\n")
f.write(f"# Score: {metadata['score']}/10\n")
f.write(f"# Opportunity: {metadata['opportunity_type']}\n")
f.write(f"# Email: {metadata['email']}\n")
f.write(f"# ---\n\n")
f.write(content)
def review_and_send_drafts(self):
"""
Interactive review process.
You approve/edit/reject each draft before sending.
"""
draft_files = list(Path("drafts/outreach").glob("*.txt"))
print(f"\n📧 {len(draft_files)} drafts to review\n")
approved = []
rejected = []
for draft_file in draft_files:
draft_content = draft_file.read_text()
print(f"\n{'='*60}")
print(draft_content)
print(f"{'='*60}\n")
decision = input("(a)pprove / (e)dit / (r)eject / (s)kip: ").lower()
if decision == 'a':
approved.append(draft_file)
elif decision == 'e':
# Open in editor, then mark as approved
os.system(f"code {draft_file}") # or 'vim', 'nano', etc.
input("Press Enter after editing...")
approved.append(draft_file)
elif decision == 'r':
rejected.append(draft_file)
# Send approved emails
for draft_file in approved:
self.send_from_draft(draft_file)
print(f"\n✅ Sent: {len(approved)}")
print(f"❌ Rejected: {len(rejected)}")
print(f"⏭️ Skipped: {len(draft_files) - len(approved) - len(rejected)}")
def send_from_draft(self, draft_file: Path):
"""Send email from approved draft."""
content = draft_file.read_text()
# Parse metadata
lines = content.split('\n')
email = [l for l in lines if l.startswith('# Email:')][0].split(': ')[1]
# Get email content (after ---)
email_content = '\n'.join(lines[lines.index('# ---')+2:])
subject = email_content.split('\n')[0].replace('Subject: ', '')
body = '\n'.join(email_content.split('\n')[2:])
# Actually send
result = send_email(to=email, subject=subject, body=body)
if result['success']:
# Move to sent folder
sent_folder = Path("sent")
sent_folder.mkdir(exist_ok=True)
draft_file.rename(sent_folder / draft_file.name)
print(f"✅ Sent to {email}")
# Usage
if __name__ == "__main__":
copilot = AISalesCopilot()
# Generate drafts (doesn't send)
copilot.generate_outreach_drafts(
niche="real estate agents",
location="Austin, Texas"
)
# Review and send (interactive)
copilot.review_and_send_drafts()

Key differences from fully automated:

  • ✅ Drafts saved to files, not sent immediately
  • ✅ Human reviews each email before sending
  • ✅ Interactive approval process
  • ✅ Can edit in your editor before sending
  • ✅ Clear audit trail (sent/ vs drafts/ folders)

The AI Co-pilot vs. Full Automation:

AspectFull Automation (Risky)AI Co-pilot (Smart)
Email sendingAutomaticHuman-approved only
Quality controlNoneHuman reviews each draft
Reputation riskHigh (one bad email = burned)Low (you catch mistakes)
Time investment2 hrs/week5-10 hrs/week
ScaleUnlimited (dangerous)Limited by review time
Trust from prospectsLow (feels spammy)High (personalized, quality)

8. What You Actually Built—and Why It Matters

8.1 Your System, in Practical Terms

You didn’t just paste together some prompts—you built a repeatable sales engine:

  • ✅ Lead pipeline: search, enrich, and score prospects automatically
  • ✅ Outreach factory: website-aware emails that sound like you, not a template
  • ✅ Follow-up cadence: nudges that add value instead of “just checking in”
  • ✅ Proposal-on-demand: clean, priced docs from real conversations

This shifts your week from prospecting to closing. Expect 5–10 focused hours instead of 40+ scattered ones.

Reality check: the system is leverage, not a lottery ticket. You’ll still need to pick a niche, tune your offer, and run calls. But now you can iterate faster than everyone else.

8.2 Your Edge (And Where It Comes From)

Compared to no‑code builders:

  • You can chain multiple steps (scrape → analyze → score → draft) without breaking
  • You can integrate deeply (CRMs, webhooks, custom logic) and charge for outcomes, not tasks
  • When things break (they will), you can actually fix them

Compared to other developers:

  • You’re treating sales as a system, not a mood
  • You automate the parts everyone else avoids
  • You ship faster because you test with 100 prospects, not 10

Is this “10×”? In learning speed, yes. In revenue, that depends on your offer and market. The point is: you now have much higher throughput.

8.3 What To Do This Week

Keep it small, real, and fast:

  1. Pick one niche + one city. Timebox to 20 minutes.
  2. Run lead gen. Expect 40% usable data—that’s enough.
  3. Send 20 emails you’d be proud to receive. Track opens/clicks.
  4. Book 2 calls or get 2 substantive replies. Learn. Adjust. Repeat.

What not to do: redesign your stack, invent a logo, or wait for “perfect.” You’re gathering signal, not building a brand.

8.4 Bottom Line

This is leverage, not luck. You’re compressing the slowest parts of sales—research, drafting, formatting—into minutes. That lets you spend time where compounding lives: conversations and delivery.

Build for yourself, use it to win, then sell the win.


Resources & Code

Tools & APIs:

Related Posts:


The code here is meant to be used, not admired. The agents are real, and the workflow holds up in practice.

Your advantage isn’t secret prompts—it’s your ability to wire research → insight → action faster than most teams can schedule a meeting. Use the agents to do the heavy lifting, then point the human effort at the moments that matter: picking a niche, shaping the offer, running the call, delivering the win.

Start: one city, one niche, 20 emails. Learn fast. Iterate faster. When you’ve got a repeatable win, turn it into your product.

Enjoyed this article? Subscribe for more!