Jump to: The Problem Prerequisites Project Setup Gmail Auth The Code Run It Extend It
The Problem This Solves

Without Agent

  • Sales rep checks email every hour
  • Manually reads every inbound enquiry
  • Mentally scores if it fits the ICP
  • Copies details into CRM by hand
  • Pings Slack manually for hot leads
  • 2–3 hrs/day on lead triage

With Agent

  • Agent monitors Gmail continuously
  • Scores lead against your ICP in seconds
  • Posts qualified leads to Slack instantly
  • Logs all leads + scores to Google Sheets
  • Drafts a personalised reply for hot leads
  • 15 min/day, zero missed hot leads
Prerequisites
Step 1 — Project Setup
  1. 1

    Create the project folder and virtual environment

bash
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
  1. 2

    Create the .env file with your credentials

.env
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.

Step 2 — Set Up Gmail API Access
This is the most involved step. Follow each sub-step carefully — it takes about 10 minutes.
  1. 1

    Create a Google Cloud project

    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.

  2. 2

    Enable the Gmail API

    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.

  3. 3

    Create OAuth credentials

    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.

  4. 4

    Run the auth flow once

    Create a file auth.py and run it once to generate a token.json file:

auth.py — run this once to authorise Gmail access
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.

Step 3 — The Agent Code
Create agent.py with the full agent logic. Read the comments to understand what each part does.
agent.py — full lead qualification agent
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()
Step 4 — Set Up the Google Sheet
  1. 1

    Create a new Google Sheet

    Go to sheets.google.com → click Blank. Name it "Lead Qualifier Log".

  2. 2

    Add headers in Row 1

    In cells A1 through H1, type these headers exactly:

    Date | From | Subject | Score | ICP Match | Pain Point | Recommended Action | Reason

  3. 3

    Copy the Sheet ID

    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.

  4. 4

    Share the sheet with your service account

    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.

Step 5 — Run and Test
  1. 1

    Send yourself a test 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.

  2. 2

    Run the agent

bash
source venv/bin/activate
python agent.py

You'll see output like:

Expected console output
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...
  1. 3

    Check Slack

    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.

  2. 4

    Check the Google 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.

Extend It

Auto-reply to high-score leads

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.

CRM integration

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.

Multi-source leads

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.

Slack approve-and-reply

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.