Library · Marketing, sales and analytics with AI

CRM on autopilot: HubSpot + AI, deals without a sales rep

Builder65 minUpdated: October 2026
67 of 105 in the library

Time: about 25 min reading + 40 min practice


The gist

A CRM is a graveyard of data nobody enters. The sales rep makes a call, nods along, jots something on a sticky note and forgets. In the system: empty fields, outdated statuses, deals stuck "in negotiation" since 2022.

Claude turns a CRM from a graveyard into a living thing: it reads the correspondence itself, updates the fields itself, writes the follow-up itself, and spots who's ready to buy right now.

🎨 Picture this: Imagine the perfect assistant sitting next to every sales rep 24/7. It listens to every conversation, fills out the customer record itself, sets a reminder for 3 days later, and suggests what to say when the customer goes quiet. That's what Claude + HubSpot MCP does.


Key concepts

  • HubSpot MCP: Claude reads and writes to HubSpot directly through tools
  • CRM auto-fill: an email arrives → Claude extracts the data → the fields update themselves
  • AI lead scoring: a 0-100 score showing who's ready to buy and who's just browsing
  • Follow-up automation: the customer hasn't replied in 3 days → Claude writes to them
  • Pipeline automation: a deal is stuck → Claude notices and suggests a way forward
  • Pipedrive as an alternative: for those who prefer a different CRM

Theory

The problem: why CRMs don't work

Many CRMs turn into an expensive notepad. Reps enter data only when someone yells at them. The result:

  • The manager doesn't know the real status of deals
  • Leads get lost: someone wrote on Friday, it was forgotten by Monday
  • No analytics, because there's no data
  • The customer gets the same email three times ("hi, we haven't connected yet")

The traditional fix is to make reps fill it in. That never works.

🎨 Picture this: It's like installing a water cooler and posting a sign: "please write down how much you drank." Nobody will.

HubSpot MCP: Claude as a CRM operator

HubSpot has an MCP server that lets Claude work with the CRM. Which actions are available (read-only, or write too) depends on the server version and the permissions you grant: see HubSpot's documentation. With enough permissions, Claude can:

  • Read contacts: who this person is, their interaction history
  • Update fields: job title, budget, deal stage
  • Create tasks: "call in 2 days"
  • Add notes: a summary of the conversation
  • Move deals: shift them between pipeline stages

The key point: Claude handles the routine on its own, without the rep. It makes sense to keep important steps (moving a deal to another stage, emails to customers) for a human to approve.

python
# HubSpot through Claude + API
import os
import time
import anthropic
import requests

HUBSPOT_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]

def update_crm_from_email(email_text: str, contact_id: str):
    client = anthropic.Anthropic()

    # Step 1: Claude extracts structured data from the email
    extraction = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=500,
        messages=[{
            "role": "user",
            "content": f"""Extract the following data from the customer's email as JSON:
            - contact_name: the customer's name
            - company: the company
            - interest: what exactly they're interested in
            - budget_hint: any hint about budget (if there is one)
            - next_step: what needs to happen next
            - urgency: urgency (low/medium/high)

            Email: {email_text}

            Return only JSON, no explanations."""
        }]
    )

    import json
    data = json.loads("".join(b.text for b in extraction.content if b.type == "text"))

    # Step 2: Update the contact in HubSpot
    headers = {
        "Authorization": f"Bearer {HUBSPOT_TOKEN}",
        "Content-Type": "application/json"
    }

    # Update the contact's properties.
    # HubSpot fills in notes_last_contacted itself (read-only), so
    # we save the email summary as a note (see the practice below), not as a property.
    update_payload = {
        "properties": {
            "hs_lead_status": "IN_PROGRESS"
        }
    }

    response = requests.patch(
        f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
        headers=headers,
        json=update_payload
    )

    # Step 3: Create a follow-up task if urgency is high
    if data.get("urgency") == "high":
        task_payload = {
            "properties": {
                "hs_task_subject": f"Urgent follow-up: {data.get('contact_name')}",
                "hs_timestamp": str(int(time.time() * 1000)),  # task due date, a required field
                "hs_task_status": "NOT_STARTED",
                "hs_task_priority": "HIGH",
                "hubspot_owner_id": "owner_id_here"
            }
        }
        requests.post(
            "https://api.hubapi.com/crm/v3/objects/tasks",
            headers=headers,
            json=task_payload
        )

    return data

AI lead scoring: who's ready to buy right now

Not all leads are equal. One is looking out of curiosity; another is calling because their boss said "buy it before the end of the quarter." Claude tells them apart by the signals in the correspondence.

Signs of high readiness:

  • Asks about specific timelines ("when can we start?")
  • Mentions competitors ("I'm comparing you with X")
  • Asks about integration ("we use SAP, how do we connect it?")
  • Names a budget or asks about pricing directly

Signs of low readiness:

  • "We're just exploring the market"
  • Nothing concrete about timing
  • Hasn't answered emails for several weeks
python
def score_lead(contact_history: str) -> dict:
    client = anthropic.Anthropic()

    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": f"""Rate this lead's readiness to buy on a scale of 0-100.

            Interaction history:
            {contact_history}

            Return JSON:
            {{
                "score": a number from 0 to 100,
                "reasoning": "a short explanation",
                "recommended_action": "what to do right now",
                "hot_signals": ["list of buying signals"],
                "risk_signals": ["list of warning signs"]
            }}"""
        }]
    )

    import json
    return json.loads("".join(b.text for b in response.content if b.type == "text"))

# Example usage
history = """
- April 15: first call, they asked about features
- April 18: they sent a list of requirements
- April 22: they asked for a demo
- April 25: after the demo they asked "what's the fastest implementation timeline?"
- April 28: they wrote "our director wants to meet next week"
"""

result = score_lead(history)
# score: 87
# recommended_action: "Confirm the meeting with the director right away and prepare a proposal"

🎨 Picture this: It's like a seasoned salesperson who can tell in 30 seconds "this one will buy" or "this one is just wasting our time." Except the AI does it for all 500 leads at once and doesn't get it wrong because of a bad mood. It can still be wrong, though, so spot-check its scores now and then.

Follow-up automation: the customer didn't reply

The most common way to lose a deal: the customer says "I'll think about it," and then silence. The rep feels awkward writing again. Or forgets.

Claude writes the follow-up itself, at the right time, in the right tone:

python
def generate_followup(
    contact_name: str,
    last_interaction: str,
    days_since_contact: int,
    deal_stage: str
) -> str:
    client = anthropic.Anthropic()

    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=400,
        messages=[{
            "role": "user",
            "content": f"""Write a follow-up email to a customer.

            Context:
            - Name: {contact_name}
            - Last contact: {last_interaction}
            - Days without a reply: {days_since_contact}
            - Deal stage: {deal_stage}

            Requirements:
            - Short (3-4 sentences)
            - Not pushy, not pleading
            - Add value (an article, an insight, a case study)
            - One clear call to action
            - English, a professional but friendly tone"""
        }]
    )

    return "".join(b.text for b in response.content if b.type == "text")

Pipeline automation: a deal is stuck

Deals get stuck. That's normal. What's not normal is nobody noticing for months.

Claude scans the pipeline every week and finds "zombie deals":

python
def audit_pipeline(deals: list) -> list:
    client = anthropic.Anthropic()

    deals_text = "\n".join([
        f"- {d['name']}: stage '{d['stage']}', "
        f"last activity {d['days_stale']} days ago, "
        f"amount {d['amount']}"
        for d in deals
    ])

    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=800,
        messages=[{
            "role": "user",
            "content": f"""Analyze the sales pipeline and flag the problem deals.

            Deals:
            {deals_text}

            For each problem deal (stuck for more than 14 days), give:
            1. Diagnosis: why it's stuck
            2. Way forward: 2-3 concrete steps
            3. Recommendation: keep going or close it as lost

            Format: a list with a heading for each deal."""
        }]
    )

    return "".join(b.text for b in response.content if b.type == "text")

Pipedrive as an alternative

If HubSpot feels expensive or complicated, take a look at Pipedrive: it's simpler and built around sales. Check both services' websites for pricing and whether there's a free plan.

Feature HubSpot Pipedrive
Free plan ✅ Yes (with limits) Check the website
MCP support ✅ Has an MCP server ⚠️ Check the documentation; the API is always available
Price See hubspot.com See pipedrive.com
Complexity High Medium
Best for Marketing + sales Sales only

The Pipedrive API is very simple, so Claude works with it easily through standard HTTP requests, even without MCP.


Practice

Assignment: set up automatic CRM updates when a new email comes in

What we're building: A customer email arrives → Claude reads it → updates the record in HubSpot by itself → creates a task if needed.

Step 1: Get a HubSpot API token

As of October 2026, HubSpot is turning off the creation of old-style apps (Private Apps): for new accounts from September 28, 2026, and for existing accounts from October 26, 2026. For server-to-server integrations, HubSpot now recommends a Service Key (in public beta as of October 2026, created through the HubSpot developer platform). The exact steps in the interface change, so follow the current documentation at developers.hubspot.com. If you already have an old app, its token works the same way: in the Authorization: Bearer ... header.

  1. Create a Service Key (or use an existing app token) with these permissions (scopes): crm.objects.contacts.read, crm.objects.contacts.write, crm.objects.deals.read, crm.objects.deals.write, plus permissions for the corresponding objects for notes and tasks
  2. Copy the token and save it in a .env file (never commit it to Git)
bash
# .env
HUBSPOT_ACCESS_TOKEN=<token from HubSpot>
ANTHROPIC_API_KEY=sk-ant-...

Step 2: Create a script that processes incoming emails

python
# email_processor.py
import os
import json
import time
import anthropic
import requests
from dotenv import load_dotenv

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
HUBSPOT_TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]

def process_incoming_email(
    email_from: str,
    email_subject: str,
    email_body: str
):
    """Processes an incoming email and updates the CRM"""

    print(f"Processing email from: {email_from}")

    # 1. Claude analyzes the email
    analysis = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=600,
        messages=[{
            "role": "user",
            "content": f"""Analyze this incoming customer email.

Subject: {email_subject}
From: {email_from}
Body: {email_body}

Return JSON with no extra text:
{{
    "intent": "inquiry/complaint/purchase_ready/follow_up/other",
    "summary": "a short 1-2 sentence summary",
    "key_points": ["list of important points"],
    "urgency": "low/medium/high",
    "sentiment": "positive/neutral/negative",
    "next_action": "a specific next action",
    "follow_up_days": number of days to wait before writing if there's no reply
}}"""
        }]
    )

    data = json.loads("".join(b.text for b in analysis.content if b.type == "text"))
    print(f"Analysis: {data['intent']}, urgency: {data['urgency']}")

    # 2. Look up the contact in HubSpot by email
    headers = {
        "Authorization": f"Bearer {HUBSPOT_TOKEN}",
        "Content-Type": "application/json"
    }

    search_response = requests.post(
        "https://api.hubapi.com/crm/v3/objects/contacts/search",
        headers=headers,
        json={
            "filterGroups": [{
                "filters": [{
                    "propertyName": "email",
                    "operator": "EQ",
                    "value": email_from
                }]
            }],
            "properties": ["email", "firstname", "lastname", "hs_lead_status"]
        }
    )

    contacts = search_response.json().get("results", [])

    if contacts:
        contact_id = contacts[0]["id"]
        contact_name = (
            contacts[0]["properties"].get("firstname", "") + " " +
            contacts[0]["properties"].get("lastname", "")
        ).strip() or email_from

        # 3. Update the contact
        update_response = requests.patch(
            f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
            headers=headers,
            json={
                "properties": {
                    "hs_lead_status": "IN_PROGRESS" if data["intent"] == "purchase_ready" else "OPEN"
                }
            }
        )

        # 4. Add a note
        note_response = requests.post(
            "https://api.hubapi.com/crm/v3/objects/notes",
            headers=headers,
            json={
                "properties": {
                    "hs_note_body": f"Email analysis:\n{data['summary']}\n\nKey points:\n" +
                                   "\n".join(f"• {p}" for p in data["key_points"]),
                    "hs_timestamp": str(int(time.time() * 1000))
                },
                "associations": [{
                    "to": {"id": contact_id},
                    "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 202}]
                }]
            }
        )

        # 5. Create a task if urgency is high
        if data["urgency"] == "high" or data["intent"] == "purchase_ready":
            task_response = requests.post(
                "https://api.hubapi.com/crm/v3/objects/tasks",
                headers=headers,
                json={
                    "properties": {
                        "hs_task_subject": f"[HOT] Reply to: {contact_name} — {email_subject}",
                        "hs_timestamp": str(int(time.time() * 1000)),
                        "hs_task_status": "NOT_STARTED",
                        "hs_task_priority": "HIGH",
                        "hs_task_body": data["next_action"]
                    }
                }
            )
            print(f"Created an urgent task for {contact_name}")

        print(f"CRM updated for {contact_name}")
        return {"status": "updated", "contact": contact_name, "analysis": data}

    else:
        # Contact not found: create a new one
        print(f"New contact: {email_from}")
        create_response = requests.post(
            "https://api.hubapi.com/crm/v3/objects/contacts",
            headers=headers,
            json={
                "properties": {
                    "email": email_from,
                    "hs_lead_status": "NEW"
                }
            }
        )
        return {"status": "created", "contact": email_from, "analysis": data}


# Test
if __name__ == "__main__":
    result = process_incoming_email(
        email_from="john@example.com",
        email_subject="We'd like to roll out your system",
        email_body="""
        Hi there,

        We're looking at several options for sales automation.
        Your product caught our interest. We have a team of 15 sales reps,
        the budget is open for discussion, and we'd like to get started this quarter.

        Could we set up a demo next week?

        John Miller, Director of Sales
        """
    )
    print(json.dumps(result, ensure_ascii=False, indent=2))

Step 3: Test it

bash
python email_processor.py

Step 4: Connect it to real email

Integration options:

  • Gmail: Google Apps Script → calls your script when a new email arrives
  • Outlook: Power Automate → HTTP request → your endpoint
  • Your own mail server: a hook when an email is received
  • Webhook service: Zapier / n8n → watches the inbox → calls the script

⚠️ Customer emails contain personal data. Send Claude only what the task needs, and check the lesson AI regulation and compliance.


Tools and resources

  • HubSpot: hubspot.com (there's a free CRM to start with)
  • HubSpot API documentation: developers.hubspot.com
  • Pipedrive: pipedrive.com (simpler than HubSpot, paid)
  • python-dotenv: for storing keys safely
  • Claude models: the model names in the code are as of October 2026; for current prices and versions, see What's current
  • n8n: for no-code orchestration (the lesson n8n + AI)
  • Zapier: for connecting Gmail/Outlook without programming

Key takeaways

CRMs don't die from bad software; they die from manual data entry. Claude takes over most of the manual entry: it reads emails, suggests field updates and creates tasks. Reps focus on talking to customers, not filling out records, and it's worth spot-checking what the AI produces.

Lead scoring turns gut feeling into a system: instead of "I think this one's ready," you get a concrete 87/100 score with reasoning and a recommendation.

Most important: this doesn't replace sales reps, it gives them a superpower. Every rep gets an AI assistant that never gets tired and never forgets anything.


Next lesson

→ The SEO machine: a full stack with Claude Code

The mark stays in this browser only and is never sent anywhere. My progress