Round-robin route HubSpot leads and notify reps in Slack using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code or another agent that supports the Agent Skills standard
  • HubSpot private app token stored as HUBSPOT_TOKEN environment variable
  • Slack Bot Token stored as SLACK_BOT_TOKEN environment variable

Overview

This approach creates an agent skill that redistributes and rebalances lead assignments. Unlike the always-on webhook approaches, this is useful for bulk reassignment -- when a rep leaves, when you add someone new to the team, or when assignments have drifted out of balance.

Step 1: Create the skill

Create .claude/skills/round-robin-leads/SKILL.md:

---
name: round-robin-leads
description: Redistribute unassigned HubSpot leads across reps via round-robin and notify in Slack
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Assign unassigned HubSpot contacts to reps via round-robin and DM each rep in Slack.
 
Usage:
- `/round-robin-leads` — assign all currently unassigned contacts
- `/round-robin-leads --rebalance` — reassign ALL contacts evenly (use when team changes)
 
Run: `python $SKILL_DIR/scripts/assign_leads.py $@`

Step 2: Write the script

Create .claude/skills/round-robin-leads/scripts/assign_leads.py:

#!/usr/bin/env python3
"""Round-robin assign HubSpot contacts and notify reps via Slack."""
import os, sys, requests
from slack_sdk import WebClient
 
HUBSPOT_TOKEN = os.environ.get("HUBSPOT_TOKEN")
SLACK_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
if not all([HUBSPOT_TOKEN, SLACK_TOKEN]):
    print("ERROR: Set HUBSPOT_TOKEN and SLACK_BOT_TOKEN")
    sys.exit(1)
 
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
slack = WebClient(token=SLACK_TOKEN)
 
REPS = [
    {"name": "Alice", "hubspot_owner_id": "12345678", "slack_user_id": "U01AAAA"},
    {"name": "Bob",   "hubspot_owner_id": "23456789", "slack_user_id": "U02BBBB"},
    {"name": "Carol", "hubspot_owner_id": "34567890", "slack_user_id": "U03CCCC"},
    {"name": "Dave",  "hubspot_owner_id": "45678901", "slack_user_id": "U04DDDD"},
]
 
rebalance = "--rebalance" in sys.argv
 
# Fetch contacts
filters = [] if rebalance else [
    {"propertyName": "hubspot_owner_id", "operator": "NOT_HAS_PROPERTY"}
]
contacts = []
after = None
while True:
    body = {
        "properties": ["firstname","lastname","email","company","jobtitle"],
        "limit": 100,
    }
    if filters:
        body["filterGroups"] = [{"filters": filters}]
    if after:
        body["after"] = after
    resp = requests.post("https://api.hubapi.com/crm/v3/objects/contacts/search", headers=HEADERS, json=body)
    resp.raise_for_status()
    data = resp.json()
    contacts.extend(data.get("results", []))
    after = data.get("paging", {}).get("next", {}).get("after")
    if not after:
        break
 
if not contacts:
    print("No contacts to assign")
    sys.exit(0)
 
print(f"Assigning {len(contacts)} contacts across {len(REPS)} reps...")
 
# Round-robin assign
assignments = {r["name"]: [] for r in REPS}
for i, contact in enumerate(contacts):
    rep = REPS[i % len(REPS)]
    props = contact["properties"]
    name = f"{props.get('firstname','')} {props.get('lastname','')}".strip()
 
    requests.patch(
        f"https://api.hubapi.com/crm/v3/objects/contacts/{contact['id']}",
        headers=HEADERS,
        json={"properties": {"hubspot_owner_id": rep["hubspot_owner_id"]}}
    ).raise_for_status()
 
    assignments[rep["name"]].append(name)
 
# Notify each rep with their new leads
for rep in REPS:
    leads = assignments[rep["name"]]
    if not leads:
        continue
    lead_list = "\n".join(f"• {l}" for l in leads[:20])
    if len(leads) > 20:
        lead_list += f"\n...and {len(leads) - 20} more"
 
    slack.chat_postMessage(
        channel=rep["slack_user_id"],
        text=f"You've been assigned {len(leads)} leads",
        blocks=[
            {"type": "section", "text": {"type": "mrkdwn",
                "text": f"📋 *{len(leads)} Leads Assigned to You*\n{lead_list}"}}
        ]
    )
 
print(f"Done. Assignments: {', '.join(f'{r['name']}: {len(assignments[r['name']])}' for r in REPS)}")

Step 3: Run it

# Assign all unassigned contacts
/round-robin-leads
 
# Rebalance all contacts evenly (after team change)
/round-robin-leads --rebalance

When to use this approach

  • A rep leaves or joins and you need to rebalance the existing book of business
  • You have a backlog of unassigned leads that need bulk routing
  • You want a one-time cleanup before turning on an always-on round-robin automation

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.