Building Your First Autonomous Sales Agent in 48 Hours
Go from zero to a working sales agent that qualifies leads, books meetings, and updates your CRM -complete weekend build guide with code, tools, and gotchas.

TL;DR
- Build a complete sales agent in one weekend that qualifies inbound leads, enriches contact data, books meetings, and updates your CRM.
- Use OpenAI Assistants API + HubSpot/Attio + Clearbit + Cal.com for a production-ready stack with a modest monthly cost at low lead volumes.
- Expect to spend most of your time defining your ICP and handling messy real-world lead data, not wiring up APIs.
Jump to Prerequisites · Jump to Hour 0-8: Foundation · Jump to Hour 8-24: Qualification · Jump to Hour 24-40: Integration · Jump to Hour 40-48: Polish
# Building Your First Autonomous Sales Agent in 48 Hours
I'll be direct: if you're manually qualifying inbound leads, you're burning time you'll never get back. Every "just checking if you're a good fit" email, every LinkedIn profile lookup, every time you copy-paste data into your CRM -that's time not spent talking to qualified prospects.
A sales agent built over a weekend can take over most of the routine inbound lead workflow. It qualifies leads based on your ICP, enriches their data with company info, scores them, and books meetings with the good ones, even while you sleep.
I'll walk through the build, the tools, the code, and the gotchas to watch for. By Sunday evening, you'll have a working agent.
Prerequisites
Before you start coding, get these sorted:
Required accounts (all have free tiers)
- OpenAI API access ($20-50/month depending on volume)
- CRM: HubSpot (free) or Attio (free up to 1,000 contacts)
- Enrichment API: Clearbit (14-day trial, then $99/month) or free alternative People Data Labs
- Calendar tool: Cal.com (free, open-source) or Calendly (free tier works)
- Email: Resend ($20/month) or SendGrid (free up to 100/day)
Technical requirements
- Python 3.10+ or Node.js 18+
- Basic API integration experience (you've used fetch/axios or requests)
- 8-12 hours of focused coding time (split across weekend)
- A real inbound lead source (website form, LinkedIn, email)
What you're building
By hour 48, your agent will:
- Monitor your lead source (form submissions, emails, CRM webhooks)
- Qualify each lead against your ICP criteria
- Enrich with firmographic data (company size, funding, tech stack)
- Score from 0-100 based on fit
- Act: Auto-book meetings for scores >75, nurture 50-75, discard <50
- Update CRM with enrichment data, score, and next actions
Importantly, it runs continuously -not triggered manually.
Hour 0-8: Foundation
Saturday morning. Coffee ready. Let's build the skeleton.
Step 1: Project setup (30 minutes)
mkdir sales-agent && cd sales-agent
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install openai hubspot-api-client clearbit python-dotenv requestsCreate .env file with your API keys:
OPENAI_API_KEY=sk-proj-...
HUBSPOT_API_KEY=pat-na1-...
CLEARBIT_API_KEY=sk_...
CAL_COM_API_KEY=cal_live_...
RESEND_API_KEY=re_...Step 2: Define your ICP (1 hour)
This is critical. Your agent can't qualify leads if you don't tell it what "qualified" means.
Create icp_criteria.py:
ICP_CRITERIA = {
"company_size": {
"min": 10,
"max": 500,
"weight": 20 # Out of 100 points
},
"industries": {
"high_fit": ["SaaS", "Technology", "Financial Services"],
"medium_fit": ["Healthcare", "E-commerce", "Manufacturing"],
"low_fit": ["Non-profit", "Education"],
"weight": 25
},
"job_titles": {
"high_fit": ["CEO", "CTO", "VP", "Director", "Head of"],
"medium_fit": ["Manager", "Lead", "Senior"],
"low_fit": ["Intern", "Student", "Consultant"],
"weight": 20
},
"tech_stack": {
"indicators": ["Salesforce", "HubSpot", "Stripe", "AWS"],
"weight": 15
},
"funding": {
"indicators": ["Series A", "Series B", "Series C", "Profitable"],
"weight": 10
},
"website_quality": {
"has_website": True,
"weight": 10
}
}
def calculate_score(lead_data: dict) -> int:
"""Calculate lead score based on ICP fit."""
score = 0
reasoning = []
# Company size
size = lead_data.get("company_size", 0)
if ICP_CRITERIA["company_size"]["min"] <= size <= ICP_CRITERIA["company_size"]["max"]:
score += ICP_CRITERIA["company_size"]["weight"]
reasoning.append(f"Company size ({size}) fits ICP")
# Industry
industry = lead_data.get("industry", "")
if industry in ICP_CRITERIA["industries"]["high_fit"]:
score += ICP_CRITERIA["industries"]["weight"]
reasoning.append(f"High-fit industry: {industry}")
elif industry in ICP_CRITERIA["industries"]["medium_fit"]:
score += ICP_CRITERIA["industries"]["weight"] * 0.6
reasoning.append(f"Medium-fit industry: {industry}")
# Job title (similar logic for other criteria)
# ... (full implementation in codebase)
return score, reasoningWhy this matters: If you skip defining criteria and hope the LLM will "figure it out", results tend to be inconsistent: the same lead can score differently on consecutive runs. Explicit rules fix this.
Step 3: Build the agent wrapper (2 hours)
Using OpenAI Assistants API instead of raw completions gives you built-in memory and tool calling.
from openai import OpenAI
import json
client = OpenAI()
# Create the sales qualification assistant
assistant = client.beta.assistants.create(
name="Sales Qualification Agent",
instructions="""You are a sales qualification agent. Your role:
1. Analyse inbound leads based on ICP criteria
2. Enrich lead data using available tools
3. Calculate qualification score
4. Recommend next action (book meeting, nurture, discard)
Always provide reasoning for your recommendations.
Be conservative -only recommend booking meetings for strong fits.""",
model="gpt-4-turbo-preview",
tools=[
{
"type": "function",
"function": {
"name": "enrich_lead",
"description": "Fetch company and contact data from Clearbit",
"parameters": {
"type": "object",
"properties": {
"email": {"type": "string"}
},
"required": ["email"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate_icp_score",
"description": "Score lead against ICP criteria",
"parameters": {
"type": "object",
"properties": {
"lead_data": {"type": "object"}
},
"required": ["lead_data"]
}
}
},
{
"type": "function",
"function": {
"name": "book_meeting",
"description": "Send calendar booking link to qualified lead",
"parameters": {
"type": "object",
"properties": {
"email": {"type": "string"},
"name": {"type": "string"}
},
"required": ["email", "name"]
}
}
}
]
)
print(f"Created assistant: {assistant.id}")Save that assistant.id in your .env file -you'll reuse it.
Step 4: Implement tool functions (3 hours)
These are the actual capabilities your agent calls.
import requests
from typing import Dict, Any
def enrich_lead(email: str) -> Dict[str, Any]:
"""Fetch enrichment data from Clearbit."""
response = requests.get(
"https://person-stream.clearbit.com/v2/combined/find",
params={"email": email},
auth=(CLEARBIT_API_KEY, '')
)
if response.status_code != 200:
return {"error": "Enrichment failed", "email": email}
data = response.json()
return {
"name": data.get("person", {}).get("name", {}).get("fullName"),
"title": data.get("person", {}).get("employment", {}).get("title"),
"company": data.get("company", {}).get("name"),
"company_size": data.get("company", {}).get("metrics", {}).get("employees"),
"industry": data.get("company", {}).get("category", {}).get("industry"),
"tech_stack": data.get("company", {}).get("tech", []),
"funding": data.get("company", {}).get("metrics", {}).get("raised"),
"website": data.get("company", {}).get("domain")
}
def book_meeting(email: str, name: str) -> Dict[str, Any]:
"""Send calendar booking link via email."""
# Get Cal.com booking link
cal_link = "https://cal.com/yourusername/discovery-call"
# Send email via Resend
response = requests.post(
"https://api.resend.com/emails",
headers={"Authorization": f"Bearer {RESEND_API_KEY}"},
json={
"from": "[email protected]",
"to": email,
"subject": "Let's chat -book a time",
"html": f"""
<p>Hi {name}, </p>
<p>Thanks for your interest! Based on what you shared, I think we'd be a great fit.</p>
<p>I'd love to show you how [your product] can help with [their pain point]. Pick a time that works for you:</p>
<p><a href="{cal_link}">Book 30-minute discovery call</a></p>
<p>Looking forward to it, <br>
[Your name]</p>
"""
}
)
return {"status": "sent", "email": email, "response": response.json()}
# Tool dispatcher
def execute_tool(tool_name: str, arguments: dict) -> dict:
"""Route tool calls to appropriate functions."""
if tool_name == "enrich_lead":
return enrich_lead(**arguments)
elif tool_name == "calculate_icp_score":
return calculate_score(**arguments)
elif tool_name == "book_meeting":
return book_meeting(**arguments)
else:
return {"error": f"Unknown tool: {tool_name}"}Step 5: Create the main agent loop (1.5 hours)
This is where it all comes together.
def process_lead(lead_email: str, lead_name: str, context: str = "") -> dict:
"""Process a single lead through the agent."""
# Create thread for this lead
thread = client.beta.threads.create()
# Send initial message
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content=f"""
New inbound lead to qualify:
Email: {lead_email}
Name: {lead_name}
Context: {context}
Please:
1. Enrich this lead's data
2. Calculate their ICP score
3. Recommend next action
4. If score > 75, book a meeting automatically
"""
)
# Run the assistant
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=ASSISTANT_ID
)
# Poll for completion and handle tool calls
while True:
run_status = client.beta.threads.runs.retrieve(
thread_id=thread.id,
run_id=run.id
)
if run_status.status == "requires_action":
# Agent is calling tools
tool_calls = run_status.required_action.submit_tool_outputs.tool_calls
tool_outputs = []
for tool_call in tool_calls:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments)
# Execute the tool
output = execute_tool(function_name, arguments)
tool_outputs.append({
"tool_call_id": tool_call.id,
"output": json.dumps(output)
})
# Submit tool outputs back to assistant
client.beta.threads.runs.submit_tool_outputs(
thread_id=thread.id,
run_id=run.id,
tool_outputs=tool_outputs
)
elif run_status.status == "completed":
# Get final response
messages = client.beta.threads.messages.list(thread_id=thread.id)
final_response = messages.data[0].content[0].text.value
return {"status": "success", "response": final_response}
elif run_status.status in ["failed", "cancelled", "expired"]:
return {"status": "error", "message": "Agent run failed"}
# Wait before polling again
time.sleep(1)Test it:
result = process_lead(
lead_email="[email protected]",
lead_name="Sarah Chen",
context="Filled out demo request form, mentioned scaling challenges"
)
print(result)Saturday checkpoint: You've got a working agent that can qualify one lead. Time for lunch.
Hour 8-24: Qualification engine
Saturday afternoon through evening. Now we make it smart.
Step 6: Add lead source monitoring (3 hours)
Your agent needs to know when new leads arrive. Three common sources:
Option A: HubSpot forms (easiest)
from hubspot import HubSpot
from hubspot.crm.contacts import ApiException
hubspot = HubSpot(access_token=HUBSPOT_API_KEY)
def fetch_new_leads():
"""Get unprocessed leads from HubSpot."""
try:
# Fetch contacts created in last hour with no "agent_processed" property
response = hubspot.crm.contacts.basic_api.get_page(
properties=["email", "firstname", "lastname", "message"],
limit=100,
archived=False
)
unprocessed = [
contact for contact in response.results
if not contact.properties.get("agent_processed")
]
return unprocessed
except ApiException as e:
print(f"HubSpot API error: {e}")
return []
def mark_lead_processed(contact_id: str, agent_result: dict):
"""Update HubSpot contact with agent's assessment."""
hubspot.crm.contacts.basic_api.update(
contact_id=contact_id,
simple_public_object_input={
"properties": {
"agent_processed": "true",
"agent_score": agent_result.get("score"),
"agent_recommendation": agent_result.get("recommendation"),
"agent_reasoning": agent_result.get("reasoning")
}
}
)Option B: Email forwarding
Use a service like Zapier or n8n to forward emails to a webhook your agent monitors.
Option C: Database polling
If leads land in a database, poll it every 5 minutes for new records.
Step 7: Build scoring logic refinement (2 hours)
The initial scoring was rigid. Add LLM-based context evaluation.
def enhanced_score_with_context(lead_data: dict, context: str) -> dict:
"""Combine rule-based scoring with LLM context analysis."""
# Start with rule-based score
base_score, base_reasoning = calculate_score(lead_data)
# Analyse context with LLM for signals
context_analysis = client.chat.completions.create(
model="gpt-3.5-turbo",
messages=[{
"role": "user",
"content": f"""
Analyse this lead's context for buying signals:
Context: "{context}"
Rate from 0-25 points based on:
- Urgency (timeline mentioned?)
- Budget awareness (cost concerns mentioned?)
- Pain points (clear problem described?)
- Decision authority (phrasing suggests decision maker?)
Return JSON: {{"context_score": 0-25, "signals": ["list", "of", "signals"]}}
"""
}]
)
context_result = json.loads(context_analysis.choices[0].message.content)
context_score = context_result["context_score"]
# Combined score
total_score = base_score + context_score
return {
"score": total_score,
"base_score": base_score,
"context_score": context_score,
"reasoning": base_reasoning + context_result["signals"]
}Step 8: Handle edge cases (2 hours)
Real-world leads are messy. Add handling for:
Missing data: Not all leads have complete info.
def handle_incomplete_lead(lead_data: dict) -> dict:
"""Deal with partial lead data gracefully."""
if not lead_data.get("email"):
return {"action": "discard", "reason": "No email provided"}
if not lead_data.get("company"):
# Try enrichment to fill gaps
enriched = enrich_lead(lead_data["email"])
if enriched.get("company"):
lead_data.update(enriched)
else:
# Can't qualify without company info
return {"action": "manual_review", "reason": "Insufficient data"}
return {"action": "proceed", "data": lead_data}Duplicate leads: Same person submits multiple forms.
def check_duplicate(email: str) -> bool:
"""Check if lead was processed recently."""
response = hubspot.crm.contacts.basic_api.get_page(
properties=["email", "agent_processed", "createdate"],
limit=1,
filters=[{"propertyName": "email", "operator": "EQ", "value": email}]
)
if response.results:
contact = response.results[0]
if contact.properties.get("agent_processed") == "true":
return True
return FalseAPI failures: Clearbit enrichment times out.
def enrich_with_retry(email: str, max_retries: int = 3) -> dict:
"""Retry enrichment on failure."""
for attempt in range(max_retries):
try:
return enrich_lead(email)
except requests.exceptions.Timeout:
if attempt == max_retries - 1:
return {"error": "Enrichment timeout", "email": email}
time.sleep(2 ** attempt) # Exponential backoffStep 9: Create agent dashboard (3 hours)
You need visibility into what your agent's doing.
Simple Flask dashboard:
from flask import Flask, render_template, jsonify
import sqlite3
app = Flask(__name__)
# Store agent actions in SQLite
def log_action(lead_email, action, score, reasoning):
conn = sqlite3.connect("agent_log.db")
conn.execute("""
INSERT INTO actions (timestamp, email, action, score, reasoning)
VALUES (datetime('now'), ?, ?, ?, ?)
""", (lead_email, action, score, reasoning))
conn.commit()
conn.close()
@app.route("/")
def dashboard():
"""Show agent activity dashboard."""
conn = sqlite3.connect("agent_log.db")
recent_actions = conn.execute("""
SELECT timestamp, email, action, score
FROM actions
ORDER BY timestamp DESC
LIMIT 50
""").fetchall()
conn.close()
return render_template("dashboard.html", actions=recent_actions)
@app.route("/stats")
def stats():
"""Agent performance stats."""
conn = sqlite3.connect("agent_log.db")
stats = conn.execute("""
SELECT
COUNT(*) as total_leads,
AVG(score) as avg_score,
SUM(CASE WHEN action = 'booked_meeting' THEN 1 ELSE 0 END) as meetings_booked,
SUM(CASE WHEN action = 'discard' THEN 1 ELSE 0 END) as discarded
FROM actions
WHERE timestamp > datetime('now', '-7 days')
""").fetchone()
conn.close()
return jsonify({
"total_leads": stats[0],
"avg_score": round(stats[1], 1),
"meetings_booked": stats[2],
"discarded": stats[3],
"conversion_rate": round(stats[2] / stats[0] * 100, 1) if stats[0] > 0 else 0
})Saturday evening checkpoint: Agent can monitor leads, qualify them, and you can see what it's doing. Take a break -you've earned it.
Hour 24-40: Integration layer
Sunday morning. Time to connect everything.
Step 10: Build the continuous processing loop (3 hours)
Your agent should run 24/7, checking for new leads every few minutes.
import schedule
import time
from datetime import datetime
def agent_main_loop():
"""Main agent processing loop."""
print(f"[{datetime.now()}] Checking for new leads...")
# Fetch unprocessed leads
new_leads = fetch_new_leads()
print(f"Found {len(new_leads)} new leads")
for lead in new_leads:
try:
# Check if duplicate
if check_duplicate(lead.properties["email"]):
print(f"Skipping duplicate: {lead.properties['email']}")
continue
# Process lead
print(f"Processing: {lead.properties['email']}")
result = process_lead(
lead_email=lead.properties["email"],
lead_name=f"{lead.properties.get('firstname', '')} {lead.properties.get('lastname', '')}".strip(),
context=lead.properties.get("message", "")
)
# Log result
log_action(
lead.properties["email"],
result.get("action"),
result.get("score"),
result.get("reasoning")
)
# Update CRM
mark_lead_processed(lead.id, result)
print(f"✓ Processed {lead.properties['email']}: {result.get('action')}")
except Exception as e:
print(f"✗ Error processing {lead.properties['email']}: {e}")
# Don't let one failure stop the loop
continue
print(f"[{datetime.now()}] Batch complete\n")
# Schedule agent to run every 5 minutes
schedule.every(5).minutes.do(agent_main_loop)
# Run immediately on start
agent_main_loop()
# Keep running
while True:
schedule.run_pending()
time.sleep(60)Step 11: Add email personalisation (2 hours)
Generic meeting invite emails convert poorly. Let the agent personalise based on lead data.
def generate_personalised_email(lead_data: dict, context: str) -> str:
"""Generate custom email using LLM."""
prompt = f"""
Write a personalised meeting invitation email for this lead:
Name: {lead_data['name']}
Title: {lead_data['title']}
Company: {lead_data['company']}
Industry: {lead_data['industry']}
Context: {context}
Requirements:
- Professional but warm tone
- Reference their industry/role specifically
- Mention relevant pain point based on context
- Keep under 100 words
- Include meeting link placeholder: {{MEETING_LINK}}
Don't oversell. Just propose a conversation.
"""
response = client.chat.completions.create(
model="gpt-4-turbo-preview",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
def send_personalised_meeting_invite(lead_data: dict, context: str):
"""Send custom meeting invite."""
email_body = generate_personalised_email(lead_data, context)
email_body = email_body.replace("{MEETING_LINK}", CAL_COM_LINK)
requests.post(
"https://api.resend.com/emails",
headers={"Authorization": f"Bearer {RESEND_API_KEY}"},
json={
"from": "[email protected]",
"to": lead_data["email"],
"subject": f"Quick chat about {lead_data['company']}'s [pain point]?",
"html": f"<p>{email_body.replace(chr(10), '</p><p>')}</p>"
}
)Personalised emails tend to book noticeably more meetings than a generic template.
Step 12: Implement nurture sequences (3 hours)
Leads scoring 50-75 aren't ready to buy now, but might be later. Add them to nurture.
def add_to_nurture_sequence(lead_data: dict, score: int):
"""Add medium-fit leads to email nurture campaign."""
# Tag in HubSpot for nurture workflow
hubspot.crm.contacts.basic_api.update(
contact_id=lead_data["contact_id"],
simple_public_object_input={
"properties": {
"lifecycle_stage": "lead",
"lead_score": score,
"nurture_sequence": "warm_leads_monthly"
}
}
)
# Or use dedicated email tool (Loops, Instantly, etc.)
requests.post(
"https://app.loops.so/api/v1/contacts/create",
headers={"Authorization": f"Bearer {LOOPS_API_KEY}"},
json={
"email": lead_data["email"],
"firstName": lead_data["name"].split()[0],
"customFields": {
"company": lead_data["company"],
"leadScore": score
}
}
)Step 13: Add Slack notifications (1 hour)
Get pinged when the agent books a meeting or encounters issues.
def send_slack_notification(message: str, channel: str = "#sales-agent"):
"""Post to Slack."""
requests.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {SLACK_BOT_TOKEN}"},
json={"channel": channel, "text": message}
)
# Call from agent loop
if result.get("action") == "booked_meeting":
send_slack_notification(
f"🔥 New meeting booked: {lead_data['name']} from {lead_data['company']} (Score: {result['score']})"
)
elif result.get("action") == "error":
send_slack_notification(
f"⚠️ Agent error processing {lead_data['email']}: {result.get('error')}"
)Step 14: Test with real leads (3 hours)
Before deploying fully, run it on 20-30 real leads manually and check:
- Are scores accurate? (Spot-check 10 leads -do you agree with scores?)
- Is enrichment working? (Check data quality in CRM)
- Are emails sending? (Verify delivery, check spam folders)
- Are meetings being booked? (Test the full flow)
Two common bugs to look for here:
- Clearbit can return
nullfor startups without funding data, which breaks a naive scoring function - Calendar links may work but show the wrong timezone (fix by adding a parameter such as
?timezone=America/Los_Angeles)
Sunday midday checkpoint: Everything's wired up. Time for final polish.
Hour 40-48: Polish and deployment
Final stretch. Make it production-ready.
Step 15: Add error handling and retries (2 hours)
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def process_lead_with_retry(lead_data):
"""Retry failed lead processing."""
return process_lead(**lead_data)
def safe_agent_loop():
"""Agent loop with error recovery."""
try:
agent_main_loop()
except Exception as e:
# Log error
print(f"Agent loop error: {e}")
send_slack_notification(f"⚠️ Agent crashed: {e}")
# Don't exit -keep running
time.sleep(60) # Wait a minute before retryingStep 16: Set up monitoring (2 hours)
Track key metrics:
import json
from datetime import datetime, timedelta
class AgentMetrics:
"""Track agent performance."""
def __init__(self):
self.metrics_file = "agent_metrics.json"
def record_event(self, event_type: str, metadata: dict = None):
"""Log an event."""
with open(self.metrics_file, "a") as f:
f.write(json.dumps({
"timestamp": datetime.now().isoformat(),
"event": event_type,
"metadata": metadata or {}
}) + "\n")
def get_daily_stats(self):
"""Calculate today's performance."""
today = datetime.now().date()
events = []
with open(self.metrics_file, "r") as f:
for line in f:
event = json.loads(line)
event_date = datetime.fromisoformat(event["timestamp"]).date()
if event_date == today:
events.append(event)
return {
"total_leads": len([e for e in events if e["event"] == "lead_processed"]),
"meetings_booked": len([e for e in events if e["event"] == "meeting_booked"]),
"errors": len([e for e in events if e["event"] == "error"]),
"avg_score": sum([e["metadata"].get("score", 0) for e in events]) / len(events) if events else 0
}
metrics = AgentMetrics()
# Record events in agent loop
metrics.record_event("lead_processed", {"email": lead_email, "score": score})
metrics.record_event("meeting_booked", {"email": lead_email, "company": company})Step 17: Deploy to production (2 hours)
Run your agent on a server, not your laptop.
Option A: Railway (easiest, $5/month)
# Install Railway CLI
npm install -g railway
# Login and deploy
railway login
railway init
railway upOption B: DigitalOcean Droplet ($4/month)
# SSH into droplet
ssh root@your-droplet-ip
# Install dependencies
apt update && apt install python3 python3-pip git
# Clone your repo
git clone https://github.com/yourusername/sales-agent
cd sales-agent
# Install requirements
pip3 install -r requirements.txt
# Run with systemd (keeps it running)
sudo nano /etc/systemd/system/sales-agent.servicesystemd service file:
[Unit]
Description=Sales Agent
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/sales-agent
ExecStart=/usr/bin/python3 agent.py
Restart=always
[Install]
WantedBy=multi-user.targetStart it:
sudo systemctl enable sales-agent
sudo systemctl start sales-agent
sudo systemctl status sales-agentStep 18: Document and handoff (2 hours)
Write a README for your team (or future you):
# Sales Agent
Autonomous lead qualification and meeting booking.
## What it does
- Monitors HubSpot for new leads every 5 minutes
- Enriches with Clearbit data
- Scores against ICP (0-100)
- Auto-books meetings for scores >75
- Adds 50-75 scores to nurture
- Discards <50
## Metrics (last 7 days)
- Total leads processed: [count]
- Meetings booked: [count] ([rate]% conversion)
- Time saved: [hours]
## Monitoring
- Dashboard: http://your-server-ip:5000
- Slack notifications: #sales-agent
- Logs: /var/log/sales-agent/
## Common issues
- "Enrichment timeout": Clearbit API is slow, agent retries automatically
- "Meeting not sent": Check Resend API credits
- "Duplicate lead": Agent correctly skipping re-processing
## Configuration
Edit `.env` for API keys
Edit `icp_criteria.py` for scoring logicSunday evening checkpoint: You're done. Your agent is running.
What to expect once it's running
The biggest changes are usually speed and consistency: leads get a response in minutes rather than days, and every lead is scored against the same criteria.
Benefits:
- No more leads slipping through cracks (agent never sleeps)
- Consistent qualification (no "I'm feeling generous today" bias)
- Data-driven ICP refinement (agent logs show which criteria actually predict deal closure)
- More complete CRM records, because every lead is enriched the same way
Gotchas to watch for:
- Enrichment credits can burn fast; People Data Labs is a cheaper alternative to Clearbit
- First drafts of emails tend to be too salesy; prompt tuning fixes it
- Calendar link timezone issues can confuse international leads; add timezone detection
Common questions
Q: What if the agent makes a mistake and discards a good lead?
A: Log everything and review discarded leads weekly. It's a quick check, and it lets you manually follow up on any the agent got wrong.
Q: Does this work if you only get 5-10 leads per week?
A: Yes, but ROI is lower. The time saved scales with volume, so at low volumes the main benefit is faster, more consistent follow-up rather than hours reclaimed. Still worth it if you value your time.
Q: Can I use this without Clearbit? It's expensive.
A: Yes. Use People Data Labs (cheaper) or Apollo API (has free tier). Enrichment quality is slightly lower but workable.
Q: What about GDPR compliance?
A: Store lead data in your CRM (which should already be GDPR-compliant). Don't store enrichment data longer than necessary. Add a line in your privacy policy about automated lead qualification.
Q: How do I improve the scoring accuracy?
A: Track which leads convert to customers, then backtest your scoring. If low-scoring leads are closing, adjust your criteria. This is iterative -expect to refine monthly.
Next steps
Week 1 after deployment:
- Monitor daily for errors
- Review agent decisions (check 10 leads/day manually)
- Gather feedback from sales team
Week 2-4:
- Analyse conversion data (which scores actually close?)
- Refine ICP criteria based on results
- A/B test email copy (agent can use multiple templates)
Month 2+:
- Add more data sources (LinkedIn, product usage if PLG)
- Build lead source scoring (which channels produce best leads?)
- Implement lead re-engagement (follow up on no-shows)
---
An agent like this frees up time for actual sales conversations. The code isn't perfect, and you'll keep finding edge cases, but once it's running it takes routine qualification off your plate.
Start this weekend. By Monday, you'll have a working sales agent handling your inbound leads whilst you focus on closing deals.
Further reading
- AI Agent Orchestration Patterns for Enterprise Workflows – How to coordinate multiple agents
- OpenAI Assistants API Documentation – Official API reference
- HubSpot CRM API Guide – CRM integration docs
- Cal.com API Documentation – Calendar booking API
External tools:
- Clearbit Enrichment API – Lead data enrichment
- People Data Labs – Alternative enrichment (cheaper)
- Resend Email API – Transactional email
- Railway – Simple deployment platform
---
Frequently Asked Questions
Q: What skills do I need to build AI agent systems?
You don't need deep AI expertise to implement agent workflows. Basic understanding of APIs, workflow design, and prompt engineering is sufficient for most use cases. More complex systems benefit from software engineering experience, particularly around error handling and monitoring.
Q: How long does it take to implement an AI agent workflow?
Implementation timelines vary based on complexity, but most teams see initial results within 2-4 weeks for simple workflows. More sophisticated multi-agent systems typically require 6-12 weeks for full deployment with proper testing and governance.
Q: What's the typical ROI timeline for AI agent implementations?
It depends on volume and how much manual work the workflow replaces. Gains usually grow over time as teams optimise prompts and workflows based on production experience.
More from the blog
How to Set Up Claude Code on a VPS: A Complete Guide
Claude Code VPS setup, step by step: provisioning, authentication, tmux vs systemd, security, and an honest look at when a VPS beats running locally.
Claude Code Agent Teams: How to Run Them on a Schedule
Claude Code Agent Teams runs up to 10 parallel Claude instances against one task list. What it is, how it works, and how to schedule runs.
Stop doing the work around the work
OpenHelm connects to your tools, reads the context, and does the steps, so you sign off on the result instead of producing it. See how it covers an entire role’s weekly workload, check the pricing, or run it yourself with the free local app.