Skip to content

How I Automated LinkedIn Prospecting with AI Agents

The Problem

I was spending 2-3 hours every day on LinkedIn prospecting. Manual tasks included:

  • Searching for prospects using filters
  • Checking each profile for fit
  • Researching company backgrounds
  • Writing personalized connection requests
  • Managing follow-ups

At 3 PM, my messages started sounding generic. My 9 AM messages were great. By late afternoon, I was just copying templates and changing names.

I calculated: 2.5 hours daily x 22 workdays = 55 hours monthly. That’s more than a full work week lost to repetitive tasks.

First Attempt: Simple Automation

I tried using PhantomBuster to send automated connection requests. The result?

phantombuster-results.txt
Day 1: Sent 50 connection requests
Day 2: 3 accepted, 47 ignored
Day 3: LinkedIn restricted my account for "suspicious activity"

My messages were generic. People could tell they were automated. The response rate was terrible.

Second Attempt: AI-Powered Personalization

I decided to build an AI agent that would:

  1. Research each prospect thoroughly
  2. Write genuinely personalized messages
  3. Queue everything for my morning review
  4. Run overnight while I slept

Here’s the architecture I designed:

architecture.txt
Prospect Discovery -> Profile Analysis -> Context Gathering -> Message Drafting -> Human Review Queue

Building the Agent with LangGraph

I chose LangGraph for its graph-based workflow. Here’s my initial implementation:

prospect_agent.py
from langgraph.graph import StateGraph, END
from typing import TypedDict, List
import anthropic
class ProspectState(TypedDict):
profile_url: str
name: str
company: str
title: str
recent_posts: List[str]
company_news: List[str]
draft_message: str
approved: bool
def analyze_profile(state: ProspectState) -> ProspectState:
"""Extract key information from LinkedIn profile"""
profile_data = scrape_linkedin_profile(state["profile_url"])
return {**state, **profile_data}
def gather_context(state: ProspectState) -> ProspectState:
"""Enrich with company and industry context"""
company_news = search_recent_news(state["company"])
recent_posts = get_linkedin_posts(state["profile_url"])
return {**state, "company_news": company_news, "recent_posts": recent_posts}
def draft_message(state: ProspectState) -> ProspectState:
"""Generate personalized outreach message"""
client = anthropic.Anthropic()
prompt = f"""Write a LinkedIn connection request message for:
Name: {state["name"]}
Title: {state["title"]}
Company: {state["company"]}
Recent posts: {state["recent_posts"][:2]}
Company news: {state["company_news"][:1]}
Guidelines:
- Keep under 300 characters
- Reference specific context (post or news)
- Be conversational, not salesy
- Ask a question to encourage response
- No generic "I'd love to connect" openings
Write only the message text."""
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
messages=[{"role": "user", "content": prompt}]
)
return {**state, "draft_message": response.content[0].text}
# Build the graph
workflow = StateGraph(ProspectState)
workflow.add_node("analyze_profile", analyze_profile)
workflow.add_node("gather_context", gather_context)
workflow.add_node("draft_message", draft_message)
workflow.set_entry_point("analyze_profile")
workflow.add_edge("analyze_profile", "gather_context")
workflow.add_edge("gather_context", "draft_message")
workflow.add_edge("draft_message", END)
agent = workflow.compile()

The Rate Limiting Problem

When I ran this overnight, I hit LinkedIn’s rate limits:

error-log.txt
ERROR: HTTP 429 - Too Many Requests
Failed after processing 15 prospects
Account flagged for review

I learned that LinkedIn limits daily actions. I added rate limiting:

rate_limited_agent.py
import time
import random
from rate_limiter import RateLimiter
def run_prospecting_agent():
prospects = load_prospect_list("target_accounts.csv")
limiter = RateLimiter(max_per_hour=20)
results = []
for prospect in prospects:
# Wait if we've hit rate limit
limiter.wait_if_needed()
try:
result = agent.invoke(prospect)
results.append(result)
# Random delay between 30-60 seconds
time.sleep(random.uniform(30, 60))
except RateLimitError:
logger.warning("Rate limit hit, pausing for 1 hour")
time.sleep(3600)
save_to_review_queue(results)

Improving Message Quality

My first AI-generated messages were better than templates, but still felt robotic:

bad-message.txt
Hi John, I noticed your recent post about AI adoption. As VP of Engineering
at TechCorp, you might be interested in our platform. Would you like to
connect and discuss?

Too generic. I improved the personalization logic:

personalization.py
def create_personalization_prompt(prospect: dict) -> str:
"""Generate context-rich prompt for message drafting"""
hooks = []
# Recent activity hook
if prospect.get("recent_posts"):
post = prospect["recent_posts"][0]
hooks.append(f"Their recent post: '{post[:100]}...'")
# Company news hook
if prospect.get("company_news"):
news = prospect["company_news"][0]
hooks.append(f"Company update: {news[:80]}")
# Role-based hook
if "VP" in prospect["title"] or "Head of" in prospect["title"]:
hooks.append("Senior leader likely focused on team efficiency")
elif "Founder" in prospect["title"]:
hooks.append("Founder focused on growth and resource optimization")
return f"""
Target: {prospect['name']} | {prospect['title']} at {prospect['company']}
Context:
{chr(10).join(hooks)}
Write a connection request that:
1. Opens with specific context (not "I noticed your profile")
2. Connects their situation to a relevant insight
3. Asks one specific question
4. Stays under 300 characters
"""

Now the messages sounded more genuine:

good-message.txt
Your post about migrating to microservices at 50 engineers resonated - that
exact inflection point where architectural decisions make or break scale.
What's been the biggest surprise in your migration journey so far?

Setting Up Overnight Processing

I configured the agent to run while I slept:

scheduler.py
import schedule
import time
import random
from datetime import datetime
def run_prospecting_agent():
print(f"Starting prospecting agent at {datetime.now()}")
prospects = load_daily_prospects()
print(f"Processing {len(prospects)} prospects")
for i, prospect in enumerate(prospects):
try:
result = agent.invoke(prospect)
queue_for_review(result)
# Rate limiting: 30-60 seconds between prospects
if i < len(prospects) - 1:
time.sleep(random.uniform(30, 60))
except Exception as e:
log_error(prospect, e)
continue
print(f"Completed at {datetime.now()}")
send_summary_email()
# Schedule for overnight execution
schedule.every().day.at("23:00").do(run_prospecting_agent)
while True:
schedule.run_pending()
time.sleep(60)

Building the Morning Review Dashboard

I never auto-send messages. Every morning, I review what the agent drafted:

review_api.py
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.route('/api/messages/queue', methods=['GET'])
def get_review_queue():
"""Fetch messages awaiting review"""
messages = db.query("""
SELECT id, prospect_name, prospect_company, draft_message,
created_at, context_sources
FROM message_queue
WHERE reviewed = FALSE
ORDER BY created_at DESC
""")
return jsonify(messages)
@app.route('/api/messages/<id>/approve', methods=['POST'])
def approve_message(id):
"""Approve message for sending"""
db.execute("""
UPDATE message_queue
SET approved = TRUE, reviewed = TRUE
WHERE id = %s
""", id)
return jsonify({"status": "approved"})
@app.route('/api/messages/<id>/edit', methods=['POST'])
def edit_message(id):
"""Edit and approve message"""
new_message = request.json.get('message')
db.execute("""
UPDATE message_queue
SET draft_message = %s, approved = TRUE, reviewed = TRUE
WHERE id = %s
""", new_message, id)
return jsonify({"status": "edited_and_approved"})

The Results

After two weeks:

results.txt
Time saved: 2-3 hours daily recovered
Response rate: 3x higher than manual outreach
Meeting bookings: 8 from 50 connection requests
Quality: Messages at 9 AM and 3 PM are equally good

The unexpected benefit: my AI-generated messages were actually better than my manual ones at 9 AM. The agent had “infinite patience” for context gathering. I would get lazy and skip reading recent posts. The agent never did.

Common Mistakes I Made

Mistake 1: Over-automation without personalization

My first attempt used templates. The AI fixed this by actually reading each prospect’s context.

Mistake 2: Ignoring LinkedIn’s limits

Sending 100+ daily requests got my account restricted. I now stay under 30 per day.

Mistake 3: Auto-sending without review

I never let the agent send directly. I review every message each morning. This caught several awkward phrasings before they went out.

Mistake 4: Using only basic profile data

Name and company are not enough. The agent now pulls recent posts, company news, and role-specific context.

Best Practices

  1. Start small - Begin with 20-30 messages daily, then scale
  2. Always review - Never auto-send without human approval
  3. Respect limits - Stay under LinkedIn’s daily thresholds
  4. Test and iterate - A/B test different message approaches
  5. Track everything - Log what works for future refinement

Summary

I built an AI agent that automates LinkedIn prospecting overnight. It saves 2-3 hours daily and produces better messages than I wrote manually. The key components are: LangGraph for workflow orchestration, Claude for message generation, rate limiting to avoid account restrictions, and a human review queue before anything goes out. The agent runs while I sleep, and I wake up to a queue of personalized messages ready for my approval.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments