Cold outreach is a numbers game—but that doesn't mean it has to feel like spam.
What if you could personally research each prospect, find their recent achievements, interests, and background, and craft a thoughtful opening message that shows you've done your homework?
That's exactly what we're building today: a tool that uses AI to automate what would normally take hours of manual research and writing. In this tutorial, I'll show you how to use AI to generate cold outreach openers that are:
- Actually personalized (not just "Hey {first_name}!")
- Based on real research (not made-up facts)
- Attention-grabbing (by referencing things your prospect actually cares about)
The best part? You can adapt this approach for your own needs—whether you're looking for a job, raising funds for your startup, or reaching out to potential clients.
Let's dive in.
Here's the high-level workflow of what we're building:
- Input: You provide basic information about your prospect (name, relevant keywords)
- Research: The AI searches the web for information about your prospect
- Analysis: The AI analyzes the search results for personalization opportunities
- Generation: The AI crafts a personalized opening message based on its research
- Output: You get a ready-to-use opening message
The entire process takes about 30-60 seconds per prospect—compared to the 15+ minutes it might take to do this research manually.
This system is built using Pocket Flow, a 100-line minimalist framework for building LLM applications. What makes Pocket Flow special isn't just its compact size, but how it reveals the inner workings of AI application development in a clear, educational way.
To follow along with this tutorial, you'll need:
- API keys for AI and search services
- Basic Python knowledge
- Git to clone the repository
Note: The implementation uses Google Search API and Claude for AI, but you can easily replace them with your preferred services such as OpenAI GPT or SerpAPI depending on your needs.
If you just want to try it out first, you can use the live demo.
Start by cloning the repository with all the code you need:
git clone https://github.com/The-Pocket/Tutorial-Cold-Email-Personalization.git
cd Tutorial-Cold-Email-Personalization
Create a .env
file in the project root directory with your API keys:
OPENAI_API_KEY=your_openai_api_key_here
The tool is designed to work with different AI and search providers. Here's a simple implementation of call_llm
using OpenAI:
# utils/call_llm.py example
import os
from openai import OpenAI
def call_llm(prompt):
"""Simple implementation using OpenAI."""
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
# Test the function
if __name__ == "__main__":
print(call_llm("Write a one-sentence greeting."))
You can easily modify this to use other AI services or add features like caching.
The search_web
utility function is implemented in a similar way—a simple function that takes a query and returns search results. Just like with the LLM implementation, you can swap in your preferred search provider (Google Search, SerpAPI, etc.) based on your needs.
Make sure your API keys work by testing the utility functions:
python utils/call_llm.py # Test your AI implementation
python utils/search_web.py # Test your search implementation
If both scripts run without errors, you're ready to go!
Install the required Python packages:
pip install -r requirements.txt
Now that you have everything set up, let's generate your first personalized opener. The tool offers multiple interfaces to fit different workflows:
- Command line interface for quick individual messages
- Web UI for a user-friendly interactive experience
- Batch processing for handling multiple prospects at scale
Choose the method that works best for your specific needs:
The simplest way to generate a single opener is through the command line:
python main.py
This will prompt you for:
- First name
- Last name
- Keywords related to the person (like company names or topics they're known for)
For a more user-friendly experience, run the web interface:
streamlit run app.py
This will open a browser window where you can:
- Enter the target person's information
- Define personalization factors to look for
- Set your preferred message style
- Generate and review the opening message
For efficiently handling multiple prospects at once, the tool provides a powerful batch processing mode:
python main_batch.py --input my_targets.csv --output my_results.csv
Your input CSV should have three columns:
first_name
: Prospect's first namelast_name
: Prospect's last namekeywords
: Space-separated keywords (e.g., "Tesla SpaceX entrepreneur")
This is particularly useful when you need to reach out to dozens or hundreds of prospects. The system will:
- Process each row in your CSV file
- Perform web searches for each prospect
- Generate personalized openers for each one
- Write the results back to your output CSV file
The output CSV will contain all your original data plus an additional column with the generated opening message for each prospect. You can then import this directly into your email marketing tool or CRM system.
Example batch processing workflow:
- Prepare a CSV with your prospect list
- Run the batch processing command
- Let it run (processing time: ~1 minute per prospect)
- Review and refine the generated openers in the output CSV
- Import into your outreach tool and start your campaign
For the best results, we recommend this approach:
- Start with single mode or the Streamlit UI to fine-tune your personalization factors and message style. This gives you immediate feedback on what works well.
- Experiment with different settings for a few test prospects until you find the perfect combination of personalization factors and style preferences.
- Once satisfied with the results, scale up using the batch processing mode to handle your entire prospect list.
This workflow ensures you don't waste time and API calls processing a large batch with suboptimal settings, and helps you refine your approach before scaling.
This system is built using Pocket Flow, a 100-line minimalist framework for building LLM applications. What makes Pocket Flow special isn't just its compact size, but how it reveals the inner workings of AI application development in a clear, educational way.
Unlike complex frameworks that hide implementation details, Pocket Flow's minimalist design makes it perfect for learning how LLM applications actually work under the hood. With just 100 lines of core code, it's impressively expressive, allowing you to build sophisticated AI workflows while still understanding every component. Despite its small size, it provides many of the same capabilities you'd find in larger libraries like LangChain, LangGraph, or CrewAI:
- Agents & Tools: Build autonomous AI agents that can use tools and make decisions
- RAG (Retrieval Augmented Generation): Enhance LLM responses with external knowledge
- Task Decomposition: Break complex tasks into manageable subtasks
- Parallel Processing: Handle multiple tasks efficiently with batch processing
- Multi-Agent Systems: Coordinate multiple AI agents working together
The difference? You can read and understand Pocket Flow's entire codebase in minutes, making it perfect for learning and customization.
Pocket Flow's approach to complex AI workflows is elegant and transparent:
- Graph-based Processing: Each task is a node in a graph, making the flow easy to understand and modify
- Shared State: Nodes communicate through a shared store, eliminating complex data passing
- Batch Processing: Built-in support for parallel processing of multiple items
- Flexibility: Easy to swap components or add new features without breaking existing code
Let's look at how we've structured our cold outreach system using Pocket Flow:
flowchart LR
A[SearchPersonNode] --> B[ContentRetrievalNode]
B --> C[AnalyzeResultsBatchNode]
C --> D[DraftOpeningNode]
classDef batch fill:#f9f,stroke:#333,stroke-width:2px
class B,C batch
The system follows a straightforward flow pattern with these core components:
- SearchPersonNode: Searches the web for information about the prospect
- ContentRetrievalNode (Batch): Retrieves and processes content from search results in parallel
- AnalyzeResultsBatchNode (Batch): Analyzes content for personalization opportunities using LLM
- DraftOpeningNode: Creates the final personalized opener
What makes this architecture powerful is its:
- Modularity: Each component can be improved independently
- Parallel Processing: Batch nodes handle multiple items simultaneously
- Flexibility: You can swap in different search providers or LLMs
- Scalability: Works for single prospects or batch processing
Now, let's break down the implementation details for each phase:
The system first searches the web for information about your prospect using their name and the keywords you provided:
# From flow.py
class SearchPersonNode(Node):
def prep(self, shared):
first_name = shared["input"]["first_name"]
last_name = shared["input"]["last_name"]
keywords = shared["input"]["keywords"]
query = f"{first_name} {last_name} {keywords}"
return query
def exec(self, query):
search_results = search_web(query)
return search_results
By default, the implementation uses Google Search API, but you can easily swap this out for another search provider like SerpAPI in the search_web
utility function. This flexibility allows you to use whichever search provider works best for your needs or budget.
Next, it retrieves and processes the content from the top search results:
class ContentRetrievalNode(BatchNode):
def prep(self, shared):
search_results = shared["search_results"]
urls = [result["link"] for result in search_results if "link" in result]
return urls
def exec(self, url):
content = get_html_content(url)
return {"url": url, "content": content}
The system then analyzes the content looking for specific personalization factors you defined:
class AnalyzeResultsBatchNode(BatchNode):
def exec(self, url_content_pair):
# Prepare prompt for LLM analysis
prompt = f"""Analyze the following webpage content about {self.first_name} {self.last_name}.
Look for the following personalization factors:
{self._format_personalization_factors(self.personalization_factors)}"""
# LLM analyzes the content for personalization opportunities
analysis_results = call_llm(prompt)
return analysis_results
Finally, the system crafts a personalized opener based on the discovered information:
class DraftOpeningNode(Node):
def exec(self, prep_data):
first_name, last_name, style, personalization = prep_data
prompt = f"""Draft a personalized opening message for a cold outreach email to {first_name} {last_name}.
Style preferences: {style}
Personalization details:
{self._format_personalization_details(personalization)}
Only write the opening message. Be specific, authentic, and concise."""
opening_message = call_llm(prompt)
return opening_message
The system uses the call_llm
utility function which can be configured to use different AI models like Claude or GPT models from OpenAI. This allows you to experiment with different LLMs to find the one that creates the most effective openers for your specific use case.
The real power of this system is in the personalization factors you define. Here are some effective examples:
- Recent company news: "I saw [Company] just announced [News]. I'd love to discuss how my experience in [Skill] could help with this initiative."
- Shared alma mater: "As a fellow [University] alum, I was excited to see your work on [Project]."
- Mutual connection: "I noticed we're both connected to [Name]. I've worked with them on [Project] and they spoke highly of your team."
- Pain points: "I noticed from your recent interview that [Company] is facing challenges with [Problem]. We've helped similar companies solve this by..."
- Growth initiatives: "Congratulations on your expansion into [Market]. Our solution has helped similar companies accelerate growth in this area by..."
- Competitor mentions: "I saw you mentioned working with [Competitor] in the past. Many of our clients who switched from them found our approach to [Feature] more effective because..."
- Investment thesis alignment: "Your recent investment in [Company] caught my attention. Our startup is also focused on [Similar Space], but with a unique approach to..."
- Industry challenges: "I read your thoughts on [Industry Challenge] in [Publication]. We're building a solution that addresses this exact issue by..."
- Shared vision: "Your talk at [Conference] about [Topic] resonated with me. We're building technology that aligns with your vision of [Vision]..."
Here are some tips for getting the best results from the system:
- Be specific with keywords: Instead of just "CEO", try "CEO FinTech YCombinator"
- Test different personalization factors: Some work better than others depending on the person
- Refine your style preferences: The more specific your style guidance, the better the results
- Review and edit: AI-generated openers are a starting point, not the final product
- A/B test: Try different approaches and track which ones get better responses
While we've focused on cold outreach openers, the same approach can be used for:
- Personalizing follow-ups after meetings
- Crafting tailored proposals based on prospect research
- Creating customized content that resonates with specific audience segments
- Building detailed prospect profiles for your sales team
The possibilities are endless when you combine AI with thoughtful personalization strategies.
The key is striking the right balance: using AI to scale your outreach without losing the human touch that makes connections meaningful.
Want to explore the full code? Check out the GitHub repository.
Have questions or want to share your results? Leave a comment below!