Route HubSpot leads by territory and company size 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_TOKENenvironment variable - Slack Bot Token stored as
SLACK_BOT_TOKENenvironment variable
Overview
This approach creates an agent skill that applies or updates territory assignments in bulk. It's useful for initial territory setup, re-routing after territory changes, or periodic cleanup of misrouted leads.
Step 1: Create the skill
Create .claude/skills/territory-routing/SKILL.md:
---
name: territory-routing
description: Route or re-route HubSpot contacts by territory and company size
disable-model-invocation: true
allowed-tools: Bash(python *)
---
Apply territory and company size routing rules to HubSpot contacts.
Usage:
- `/territory-routing` — route all unassigned contacts
- `/territory-routing --all` — re-route all contacts (use after territory changes)
- `/territory-routing --dry-run` — show what would change without writing
Run: `python $SKILL_DIR/scripts/route_territories.py $@`Step 2: Write the script
Create .claude/skills/territory-routing/scripts/route_territories.py:
#!/usr/bin/env python3
"""Route HubSpot contacts by territory and company size."""
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 HUBSPOT_TOKEN:
print("ERROR: Set HUBSPOT_TOKEN"); sys.exit(1)
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
slack = WebClient(token=SLACK_TOKEN) if SLACK_TOKEN else None
# --- Territory config (edit this) ---
TERRITORY_CONFIG = {
"enterprise_threshold": 1000,
"enterprise_rep": {"owner_id": "444444", "slack_id": "U04DDDD", "name": "Dave"},
"territories": {
"NY": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
"MA": {"owner_id": "111111", "slack_id": "U01AAAA", "name": "Alice"},
"CA": {"owner_id": "222222", "slack_id": "U02BBBB", "name": "Bob"},
"WA": {"owner_id": "222222", "slack_id": "U02BBBB", "name": "Bob"},
"FL": {"owner_id": "333333", "slack_id": "U03CCCC", "name": "Carol"},
"TX": {"owner_id": "333333", "slack_id": "U03CCCC", "name": "Carol"},
},
"default_rep": {"owner_id": "555555", "slack_id": "U05EEEE", "name": "Eve"},
}
dry_run = "--dry-run" in sys.argv
route_all = "--all" in sys.argv
# Fetch contacts
filters = [] if route_all else [
{"propertyName": "hubspot_owner_id", "operator": "NOT_HAS_PROPERTY"}
]
contacts = []
after = None
while True:
body = {
"properties": ["firstname","lastname","email","company","state","country","numberofemployees","hubspot_owner_id"],
"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 route"); sys.exit(0)
print(f"{'[DRY RUN] ' if dry_run else ''}Routing {len(contacts)} contacts...\n")
# Route each contact
results = {"enterprise": 0, "territory": 0, "fallback": 0}
for contact in contacts:
props = contact["properties"]
state = (props.get("state") or "").upper()
employees = int(props.get("numberofemployees") or 0)
name = f"{props.get('firstname','')} {props.get('lastname','')}".strip()
if employees >= TERRITORY_CONFIG["enterprise_threshold"]:
rep = TERRITORY_CONFIG["enterprise_rep"]
reason = f"Enterprise ({employees} emp)"
results["enterprise"] += 1
elif state in TERRITORY_CONFIG["territories"]:
rep = TERRITORY_CONFIG["territories"][state]
reason = f"Territory: {state}"
results["territory"] += 1
else:
rep = TERRITORY_CONFIG["default_rep"]
reason = f"Fallback (state: {state or '?'})"
results["fallback"] += 1
print(f" {name} -> {rep['name']} ({reason})")
if not dry_run:
requests.patch(
f"https://api.hubapi.com/crm/v3/objects/contacts/{contact['id']}",
headers=HEADERS,
json={"properties": {"hubspot_owner_id": rep["owner_id"]}}
).raise_for_status()
print(f"\nSummary: {results['enterprise']} enterprise, {results['territory']} territory, {results['fallback']} fallback")
if dry_run:
print("(Dry run — no changes written)")Step 3: Run it
# Preview routing decisions without writing
/territory-routing --dry-run
# Route all unassigned contacts
/territory-routing
# Re-route all contacts after territory changes
/territory-routing --allWhen to use this approach
- You're setting up territories for the first time and need to route existing contacts in bulk
- A territory change happened (new rep, region reassignment) and you need to re-route
- You want to audit routing with
--dry-runbefore making changes
Need help implementing this?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.