Watches your Gmail inbox for inbound leads, scores them against your ideal customer profile using GPT-4, routes qualified leads to Slack, and logs everything to a Google Sheet — fully automated.
Check your version: python3 --version. If you don't have Python, download it from python.org. On a Mac, you can also install it via Homebrew: brew install python.
Go to platform.openai.com/api-keys → Create new secret key. Save it — you'll only see it once. Add $5 of credit to your account (more than enough for months of testing). Alternatively, use Claude: get a key at console.anthropic.com — the code below shows both options.
You need this to read your Gmail inbox. Steps are in the Gmail Auth section below — it takes about 10 minutes.
Go to api.slack.com/apps → Create New App → From scratch. Name it "Lead Qualifier". Under Incoming Webhooks → Activate. Click Add New Webhook to Workspace → pick the channel (e.g. #leads) → copy the webhook URL. It looks like https://hooks.slack.com/services/T.../B.../xxx.
In the same Google Cloud project you'll create below, also enable the Google Sheets API. You'll create a spreadsheet with headers: Date | From | Subject | Score | ICP Match | Summary | Action Taken.
mkdir lead-qualifier && cd lead-qualifier
python3 -m venv venv
source venv/bin/activate # Mac/Linux
# venv\Scripts\activate # Windows
pip install openai google-auth google-auth-oauthlib \
google-api-python-client gspread requests python-dotenv
OPENAI_API_KEY=sk-...your-key-here...
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../xxx
GOOGLE_SHEET_ID=your-google-sheet-id-from-the-url
# Gmail credentials come from the credentials.json file (see next step)
Important: Add .env and credentials.json to a .gitignore file so you never accidentally commit secrets to GitHub.
Go to console.cloud.google.com. Click the project dropdown at the top → New Project. Name it "lead-qualifier". Click Create. Make sure it's selected in the dropdown.
In the left sidebar, go to APIs & Services → Library. Search for "Gmail API". Click it → click Enable. Also enable the Google Sheets API the same way.
Go to APIs & Services → Credentials → Create Credentials → OAuth client ID. If prompted to configure the consent screen first, click Configure consent screen → External → Create. Fill in the app name ("Lead Qualifier"), your email, and click Save and Continue through the rest. Then go back to Credentials → Create Credentials → OAuth client ID → Desktop app. Click Create. Download the JSON file and save it as credentials.json in your project folder.
Create a file auth.py and run it once to generate a token.json file:
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/spreadsheets'
]
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
with open('token.json', 'w') as f:
f.write(creds.to_json())
print("Auth successful! token.json created.")
Run it: python auth.py. A browser window opens. Sign in with your Google account and grant access. A token.json file is created. You won't need to do this again unless you revoke access.
agent.py with the full agent logic. Read the comments to understand what each part does.import os
import base64
import json
import time
import requests
from dotenv import load_dotenv
from openai import OpenAI
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
import gspread
load_dotenv()
# ── Configuration ────────────────────────────────────────────────────────────
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
SLACK_WEBHOOK = os.getenv("SLACK_WEBHOOK_URL")
SHEET_ID = os.getenv("GOOGLE_SHEET_ID")
# Your Ideal Customer Profile — edit this to match your business
ICP = """
Our ideal customer:
- Company size: 50–5000 employees (SMB to mid-market)
- Industry: SaaS, fintech, or professional services
- Role of sender: VP, Director, or C-suite
- Has an urgent, specific problem they want to solve
- Mentions a budget or timeline
Signals of a BAD fit (score low):
- Students or individuals (not businesses)
- Agencies looking to resell
- Job seekers or recruiters
- Vague messages with no clear need
- Competitors researching us
"""
# ── Gmail helpers ─────────────────────────────────────────────────────────────
def get_gmail_service():
creds = Credentials.from_authorized_user_file('token.json')
return build('gmail', 'v1', credentials=creds)
def get_unread_emails(service, max_results=20):
"""Fetch unread emails from Gmail inbox."""
results = service.users().messages().list(
userId='me',
q='is:unread in:inbox',
maxResults=max_results
).execute()
return results.get('messages', [])
def get_email_details(service, msg_id):
"""Get full email content for a given message ID."""
msg = service.users().messages().get(
userId='me', id=msg_id, format='full'
).execute()
headers = {h['name']: h['value'] for h in msg['payload']['headers']}
subject = headers.get('Subject', '(no subject)')
sender = headers.get('From', 'unknown')
# Extract plain text body
body = ''
parts = msg['payload'].get('parts', [msg['payload']])
for part in parts:
if part.get('mimeType') == 'text/plain':
data = part['body'].get('data', '')
body = base64.urlsafe_b64decode(data).decode('utf-8', errors='ignore')
break
return {'id': msg_id, 'subject': subject, 'sender': sender, 'body': body[:3000]}
def mark_as_read(service, msg_id):
"""Mark an email as read after processing."""
service.users().messages().modify(
userId='me', id=msg_id,
body={'removeLabelIds': ['UNREAD']}
).execute()
# ── Lead scoring with GPT-4 ───────────────────────────────────────────────────
def score_lead(email):
"""Use GPT-4 to score a lead against the ICP and extract structured data."""
prompt = f"""
You are a lead qualification expert. Analyse this inbound email and score
it against our Ideal Customer Profile.
ICP DEFINITION:
{ICP}
EMAIL FROM: {email['sender']}
SUBJECT: {email['subject']}
BODY:
{email['body']}
Respond ONLY with valid JSON in this exact format:
{{
"score": ,
"icp_match": "",
"company_size_guess": "",
"sender_role_guess": "",
"pain_point": "",
"urgency": "",
"recommended_action": "",
"reply_draft": "= 7, otherwise empty string>",
"reason": "<2-sentence explanation of the score>"
}}
"""
response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
# ── Slack notification ────────────────────────────────────────────────────────
def notify_slack(email, scoring):
"""Post a Slack message for qualified leads (score >= 7)."""
score = scoring['score']
emoji = "🔥" if score >= 9 else "✅" if score >= 7 else "📧"
color = "#36a64f" if score >= 7 else "#daa520"
payload = {
"attachments": [{
"color": color,
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": f"{emoji} New Lead — Score {score}/10"}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*From:*\n{email['sender']}"},
{"type": "mrkdwn", "text": f"*ICP Match:*\n{scoring['icp_match']}"},
{"type": "mrkdwn", "text": f"*Subject:*\n{email['subject']}"},
{"type": "mrkdwn", "text": f"*Action:*\n{scoring['recommended_action']}"},
]
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Pain point:* {scoring['pain_point']}"}
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Why this score:* {scoring['reason']}"}
}
]
}]
}
if scoring.get('reply_draft'):
payload["attachments"][0]["blocks"].append({
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Suggested reply:*\n_{scoring['reply_draft']}_"}
})
requests.post(SLACK_WEBHOOK, json=payload)
# ── Google Sheets logging ─────────────────────────────────────────────────────
def log_to_sheet(email, scoring):
"""Append a row to the Google Sheet with the lead data."""
from datetime import datetime
creds = Credentials.from_authorized_user_file('token.json')
client = gspread.authorize(creds)
sheet = client.open_by_key(SHEET_ID).sheet1
row = [
datetime.now().strftime("%Y-%m-%d %H:%M"),
email['sender'],
email['subject'],
scoring['score'],
scoring['icp_match'],
scoring['pain_point'],
scoring['recommended_action'],
scoring['reason']
]
sheet.append_row(row)
# ── Main loop ─────────────────────────────────────────────────────────────────
def run_agent():
print("Lead Qualifier Agent starting...")
gmail_service = get_gmail_service()
# Ensure sheet has headers on first run
# (manually add headers to your sheet: Date | From | Subject | Score |
# ICP Match | Pain Point | Action | Reason)
while True:
print(f"Checking inbox...")
emails = get_unread_emails(gmail_service)
print(f"Found {len(emails)} unread emails")
for msg_ref in emails:
try:
email = get_email_details(gmail_service, msg_ref['id'])
scoring = score_lead(email)
print(f" {email['sender'][:40]} → Score: {scoring['score']}/10 "
f"({scoring['icp_match']})")
# Notify Slack for qualified leads (score 7+)
if scoring['score'] >= 7:
notify_slack(email, scoring)
# Log everything to Google Sheets
log_to_sheet(email, scoring)
# Mark email as read so we don't process it again
mark_as_read(gmail_service, email['id'])
except Exception as e:
print(f" Error processing {msg_ref['id']}: {e}")
print("Sleeping 5 minutes before next check...")
time.sleep(300) # Check every 5 minutes
if __name__ == "__main__":
run_agent()
Go to sheets.google.com → click Blank. Name it "Lead Qualifier Log".
In cells A1 through H1, type these headers exactly:
Date | From | Subject | Score | ICP Match | Pain Point | Recommended Action | Reason
Look at the URL: https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms/edit. The long string between /d/ and /edit is your Sheet ID. Paste it into the .env file.
If you're using OAuth (which we are), this step is already handled — gspread uses your personal OAuth token. If you ever switch to a service account approach, you'd share the sheet with the service account email.
Before running the agent, send 2–3 test emails to your inbox with different scenarios: one that should qualify (e.g. "Hi, I'm VP of Sales at Acme Corp, we have 500 reps and need a solution for..."), one that should be disqualified (e.g. "Hi, I'm a student looking for internship advice"), and one that's ambiguous.
source venv/bin/activate
python agent.py
You'll see output like:
Lead Qualifier Agent starting...
Checking inbox...
Found 3 unread emails
sarah.jones@acmecorp.com → Score: 8/10 (High)
student123@gmail.com → Score: 2/10 (Low)
info@vendor.com → Score: 5/10 (Medium)
Sleeping 5 minutes before next check...
The high-score lead (Sarah from Acme) should appear in your Slack channel with a formatted card showing the score, pain point, and suggested reply. The low-score leads won't appear in Slack but will be logged to the sheet.
Open your Google Sheet. All three emails should be logged with their scores and analysis. This is your full lead pipeline view.
Run continuously: To keep the agent running in the background, use nohup python agent.py & disown on a server, or deploy it to a free tier on Railway, Fly.io, or Render. On a Mac, you can run it in a persistent terminal session or set up a cron job.
When score >= 8, use the Gmail API to send the reply_draft automatically. Add a 30-minute delay so it doesn't feel robotic. Useful for after-hours lead capture.
Instead of (or in addition to) Google Sheets, use the HubSpot or Salesforce API to create a contact and opportunity automatically for every qualified lead.
Add a webhook endpoint (using Flask) that accepts leads from your website contact form, LinkedIn Lead Gen Forms, or a Typeform — and runs the same scoring logic.
Add interactive buttons to the Slack message: "Send reply" / "Edit first" / "Disqualify". Use Slack's Block Kit and a Flask webhook to handle button clicks and trigger the email send.