Building Your AI-Powered Sales Engine Part 2: From Lead Generation to Client Acquisition
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:
- Search Google Maps for businesses
- Extract website URLs
- Scrape each website for signals
- Use LLM to identify automation opportunities
- Score and rank leads
- Generate personalized outreach angles
2.2 Architecture Overview
Here’s how the Lead Generation Agent flows through its decision-making process:
Lead Generation Agent state machine workflow
Code implementation:
from typing import TypedDict, List, Dict, Optionalfrom langgraph.graph import StateGraph, ENDfrom langchain_google_genai import ChatGoogleGenerativeAIfrom langchain_tavily import TavilySearchfrom pydantic import BaseModel, Fieldimport requestsfrom bs4 import BeautifulSoupimport 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 need2. pain_point: What manual process is causing them pain3. solution_angle: How your automation solves it (be specific!)4. estimated_value: How much time/money this saves them5. 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 website2. 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 video5. 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()
# Usageif __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 graphdef 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 positivelyconversation = """Lead: "Hi, I watched your demo. This could be useful. We get about20 form submissions daily and manually enter them into Salesforce.Takes about 30 minutes. Can you also send them a welcome emailautomatically?"
You: "Absolutely. The automation would capture submissions in real-time,add them to Salesforce with proper tags, and send personalized welcomeemails. 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 proposalwith 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:
Complete AI Sales Pipeline architecture showing all three agents
Data flow:
- Lead Gen Agent → Outputs scored leads with automation opportunities
- Cold Email Agent → Takes top leads, sends personalized outreach, tracks engagement
- Proposal Agent → Triggered by positive responses, generates professional quotes
- 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 osfrom sendgrid import SendGridAPIClientfrom 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 processingdef 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. Cachingfrom 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 tasksdef 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'] }
# Usageperformance = EmailPerformance()
# When sendingperformance.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 resultsbest = 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:
- Lead Generator: Finds 100 qualified prospects in 1 hour
- Outreach Writer: Creates personalized emails at scale
- Follow-up Manager: Nurtures leads automatically
- 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:
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 agentleads = lead_gen_agent.invoke({ "niche": "your_niche", "location": "target_city", "automation_type": "your_service"})
# Export to CRM or CSV for reviewexport_leads(leads['scored_leads'][:50])
Tuesday: Review & approve outreach
# AI generates drafts, saves to review folderfor 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 draftsprint("📋 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 emailsapproved_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 reviewingsend_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 SystemGenerates leads, drafts outreach, manages follow-upsIMPORTANT: Humans review before sending"""
import osfrom datetime import datetimefrom typing import List, Dictfrom 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}")
# Usageif __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:
Aspect | Full Automation (Risky) | AI Co-pilot (Smart) |
---|---|---|
Email sending | Automatic | Human-approved only |
Quality control | None | Human reviews each draft |
Reputation risk | High (one bad email = burned) | Low (you catch mistakes) |
Time investment | 2 hrs/week | 5-10 hrs/week |
Scale | Unlimited (dangerous) | Limited by review time |
Trust from prospects | Low (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:
- Pick one niche + one city. Timebox to 20 minutes.
- Run lead gen. Expect 40% usable data—that’s enough.
- Send 20 emails you’d be proud to receive. Track opens/clicks.
- 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:
- Tavily Search API - Web search for agents
- SendGrid - Email delivery and tracking
- LangChain - Agent orchestration
- LangGraph - State machine workflows
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!
Related Articles
AI Automation Business for Developers - Part 1
How developers can dominate the automation market by building AI agents they'd actually use - and turning them into profitable services

Dokku Migrations: Introducing the Open Source Dokku Migration Tool
Learn about Dokku Migrations: Introducing the Open Source Dokku Migration Tool

Extending LLM Capabilities with Custom Tools: Beyond the Knowledge Cutoff
Learn about Extending LLM Capabilities with Custom Tools: Beyond the Knowledge Cutoff

A Year of Learning Generative AI: A Software Engineer''s Journey
Learn about A Year of Learning Generative AI: A Software Engineer''s Journey