Round-robin route HubSpot leads and notify reps in Slack using a Claude Code skill
Install this skill
Download the skill archive and extract it into your .claude/skills/ directory.
round-robin-leads.skill.zipPrerequisites
This skill works with any agent that supports the Claude Code skills standard, including Claude Code, Claude Cowork, OpenAI Codex, and Google Antigravity.
- One of the agents listed above
- HubSpot private app with
crm.objects.contacts.readandcrm.objects.contacts.writescopes - Slack app with
chat:writepermission
Why a Claude Code skill?
The other approaches in this guide are automated: they route every new lead the same way, every time. An Claude Code skill is different. You tell Claude what you want in plain language, and the skill gives it enough context to do it reliably.
That means you can say:
- "Assign all unassigned leads via round-robin and notify the reps"
- "A rep just left — rebalance all leads evenly across the remaining team"
- "How are leads distributed right now? Show me the breakdown by rep"
The skill contains workflow guidelines, API reference materials, and a rep roster template that the agent reads on demand. When you invoke the skill, Claude reads these files, writes a script on the fly, runs it, and reports results. If you ask for something different next time — a rebalance, a distribution report, adding a new rep — the agent adapts without you touching any code.
How it works
The skill directory has three parts:
SKILL.md— workflow guidelines telling the agent what steps to follow, which env vars to use, and what pitfalls to avoidreferences/— HubSpot API patterns (contact search, owner update, batch update) and Slack API patterns so the agent calls the right endpointstemplates/— a rep roster template and Slack message format
When invoked, the agent reads SKILL.md, consults the reference and template files as needed, writes a Python script, executes it, and reports what it assigned. The reference files act as guardrails — the agent knows exactly which endpoints to hit and what the responses look like, so it doesn't have to guess.
What is a Claude Code skill?
An Claude Code skill is a reusable command you add to your project that Claude Code can run on demand. Skills live in a .claude/skills/ directory and are defined by a SKILL.md file that tells the agent what the skill does, when to run it, and what tools it's allowed to use.
In this skill, the agent doesn't run a pre-written script. Instead, SKILL.md provides workflow guidelines and points to reference files — API documentation, roster templates — that the agent reads to generate and execute code itself. This is the key difference from a traditional script: the agent can adapt its approach based on what you ask for while still using the right APIs and data formats.
Once installed, you can invoke a skill as a slash command (e.g., /round-robin-leads), or the agent will use it automatically when you give it a task where the skill is relevant. Skills are portable — anyone who clones your repo gets the same commands.
Step 1: Create the skill directory
mkdir -p .claude/skills/round-robin-leads/{templates,references}This creates the layout:
.claude/skills/round-robin-leads/
├── SKILL.md # workflow guidelines + config
├── templates/
│ └── rep-roster.md # Rep roster and Slack message template
└── references/
└── hubspot-contacts-api.md # HubSpot CRM search, update, and batch API patternsStep 2: Write the SKILL.md
Create .claude/skills/round-robin-leads/SKILL.md:
---
name: round-robin-leads
description: Assign unassigned HubSpot leads to reps via round-robin and notify in Slack
disable-model-invocation: true
allowed-tools: Bash, Read
---
## Goal
Assign unassigned HubSpot contacts to reps via round-robin and DM each rep in Slack with their new leads. Default: assign only unassigned contacts. The user may request a full rebalance of all contacts.
## Configuration
Read these environment variables:
- `HUBSPOT_ACCESS_TOKEN` — HubSpot private app token (required)
- `SLACK_BOT_TOKEN` — Slack bot token with chat:write scope (required)
- `HUBSPOT_PORTAL_ID` — HubSpot portal ID for contact links (required)
The rep roster is defined in `templates/rep-roster.md`. Read it for the list of reps with their HubSpot owner IDs and Slack user IDs.
## Workflow
1. Validate that `HUBSPOT_ACCESS_TOKEN`, `SLACK_BOT_TOKEN`, and `HUBSPOT_PORTAL_ID` are set. If any are missing, print the error and exit.
2. Read the rep roster from `templates/rep-roster.md`.
3. Search for contacts to assign. Default: filter to contacts without `hubspot_owner_id`. If the user requests a rebalance, fetch all contacts. See `references/hubspot-contacts-api.md`.
4. Assign contacts via round-robin — cycle through the rep roster evenly using modular indexing.
5. Update each contact's `hubspot_owner_id` in HubSpot. For large volumes (50+), use the batch update endpoint. See `references/hubspot-contacts-api.md`.
6. DM each rep in Slack with a summary of their newly assigned leads (names, companies, HubSpot links). See `templates/rep-roster.md` for the message format.
7. Print a summary: total contacts assigned, count per rep, and whether this was a new assignment or rebalance.
## Important notes
- When rebalancing, warn the user first: "This will reassign ALL contacts, including ones reps are already working. Proceed?" Wait for confirmation before executing.
- Use the batch update endpoint for 50+ contacts to stay within HubSpot's rate limits (150 requests per 10 seconds).
- The rep roster in the template must be updated with real HubSpot owner IDs and Slack user IDs before running.
- Use the `requests` library for HTTP calls. Install with pip if needed.
- Paginate search results using the `after` cursor from `paging.next.after`.
- Cap the Slack message at 20 lead names per rep. If a rep gets more than 20, show the first 20 and add "...and N more."Understanding the SKILL.md
| Section | Purpose |
|---|---|
| Goal | Tells the agent what outcome to produce |
| Configuration | Which env vars to read and where to find the rep roster |
| Workflow | Numbered steps with pointers to reference files |
| Important notes | Non-obvious context that prevents common mistakes |
Step 3: Add reference files
references/hubspot-contacts-api.md
Create .claude/skills/round-robin-leads/references/hubspot-contacts-api.md:
# HubSpot Contacts API Reference
## Authentication
```
Authorization: Bearer <HUBSPOT_ACCESS_TOKEN>
Content-Type: application/json
```
## Search for unassigned contacts
**Request:**
```
POST https://api.hubapi.com/crm/v3/objects/contacts/search
Authorization: Bearer <token>
Content-Type: application/json
```
**Body (unassigned contacts only):**
```json
{
"filterGroups": [
{
"filters": [
{
"propertyName": "hubspot_owner_id",
"operator": "NOT_HAS_PROPERTY"
}
]
}
],
"properties": ["firstname", "lastname", "email", "company", "jobtitle"],
"limit": 100
}
```
**Body (all contacts — for rebalancing):**
```json
{
"properties": ["firstname", "lastname", "email", "company", "jobtitle", "hubspot_owner_id"],
"limit": 100
}
```
**Response shape:**
```json
{
"total": 45,
"results": [
{
"id": "12345",
"properties": {
"firstname": "Jamie",
"lastname": "Park",
"email": "jamie@northwind.com",
"company": "Northwind Traders",
"jobtitle": "Marketing Manager"
}
}
],
"paging": {
"next": {
"after": "100"
}
}
}
```
Paginate using `after` cursor until `paging.next` is absent.
## Update a contact's owner
**Request:**
```
PATCH https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}
Authorization: Bearer <token>
Content-Type: application/json
```
**Body:**
```json
{
"properties": {
"hubspot_owner_id": "12345678"
}
}
```
## Batch update (for large volumes)
```
POST https://api.hubapi.com/crm/v3/objects/contacts/batch/update
```
**Body:**
```json
{
"inputs": [
{"id": "12345", "properties": {"hubspot_owner_id": "12345678"}},
{"id": "67890", "properties": {"hubspot_owner_id": "23456789"}}
]
}
```
Up to 100 contacts per batch request. Use this for 50+ contacts to stay within rate limits.
## Rate limits
150 requests per 10 seconds for private apps. For large rebalances, use the batch endpoint.templates/rep-roster.md
Create .claude/skills/round-robin-leads/templates/rep-roster.md:
# Rep Roster
Update this list with your real HubSpot owner IDs and Slack user IDs.
## Reps
| Name | HubSpot Owner ID | Slack User ID |
|------|-------------------|---------------|
| Alice | 12345678 | U01AAAA |
| Bob | 23456789 | U02BBBB |
| Carol | 34567890 | U03CCCC |
| Dave | 45678901 | U04DDDD |
Find HubSpot owner IDs in Settings > Users & Teams.
Find Slack user IDs by clicking a profile > three dots > Copy member ID.
## Slack notification format
Send each rep a DM summarizing their newly assigned leads:
```
📋 *{count} Leads Assigned to You*
• Jamie Park — Marketing Manager at Northwind Traders (View: <hubspot_link>)
• Alex Rivera — VP of Engineering at TechCorp (View: <hubspot_link>)
...and {overflow_count} more
```
HubSpot link format: `https://app.hubspot.com/contacts/{PORTAL_ID}/contact/{contact_id}`
Cap at 20 lead names per message. If more than 20, add "...and N more" at the end.Step 4: Test the skill
Invoke the skill conversationally:
/round-robin-leadsClaude will read the SKILL.md, check the reference files, write a script, run it, and report the results. A typical run looks like:
Assigning 12 unassigned contacts across 4 reps...
Alice: 3 leads (Jamie Park, Alex Rivera, Sam Ortiz)
Bob: 3 leads (Morgan Lee, Taylor Kim, Jordan Cruz)
Carol: 3 leads (Casey Patel, Avery Singh, Drew Nakamura)
Dave: 3 leads (Riley Zhao, Quinn Adams, Blake Fernandez)
Done. Assigned 12 contacts. Each rep was DM'd in Slack.Because the agent generates code on the fly, you can also make ad hoc requests:
- "A rep just left — rebalance all leads across Alice, Bob, and Carol" — the agent redistributes, skipping the removed rep
- "How are leads distributed right now?" — the agent queries and reports without reassigning
- "Assign only leads from the last 7 days" — the agent adds a date filter
Start with 5-10 unassigned contacts to verify the round-robin distribution and Slack DMs work correctly before running on your full contact list.
Step 5: Schedule it (optional)
Option A: Cron + Claude CLI
# Assign unassigned leads every hour
0 * * * * cd /path/to/your/project && claude -p "Run /round-robin-leads" --allowedTools 'Bash(*)' 'Read(*)'Option B: GitHub Actions + Claude
name: Lead Round Robin
on:
schedule:
- cron: '0 * * * *' # Every hour
workflow_dispatch: {} # Manual trigger for testing
jobs:
assign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: anthropics/claude-code-action@v1
with:
prompt: "Run /round-robin-leads"
allowed_tools: "Bash(*),Read(*)"
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
HUBSPOT_PORTAL_ID: ${{ secrets.HUBSPOT_PORTAL_ID }}Option C: Cowork Scheduled Tasks
Claude Desktop's Cowork supports built-in scheduled tasks. Open a Cowork session, type /schedule, and configure the cadence — hourly, daily, weekly, or weekdays only. Each scheduled run has full access to your connected tools, plugins, and MCP servers.
Scheduled tasks only run while your computer is awake and Claude Desktop is open. If a run is missed, Cowork executes it automatically when the app reopens. For always-on scheduling, use GitHub Actions (Option B) instead. Available on all paid plans (Pro, Max, Team, Enterprise).
Troubleshooting
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 distribution report before a pipeline review meeting
- You're doing a one-time cleanup before turning on an always-on round-robin automation
When to switch approaches
- You need real-time routing of every new lead as it arrives → use n8n or Code + webhook
- You want a visual workflow builder → use n8n or Make
- You want the simplest setup → use Zapier
Common questions
Why not just use a script?
A script runs the same way every time. The Claude Code skill adapts to what you ask — assign only unassigned leads, rebalance all leads, filter by date range, generate a distribution report, add or remove reps on the fly. The reference files ensure it calls the right APIs even when improvising, so you get flexibility without sacrificing reliability.
Does this use Claude API credits?
Yes. Unlike a script-based approach, the agent reads skill files and generates code each time. Typical cost is $0.01-0.05 per invocation. The HubSpot and Slack APIs themselves are free.
Can I use this alongside always-on routing?
Yes. Use n8n or webhooks for real-time routing of new leads, and the Claude Code skill for periodic rebalancing, bulk reassignment, or distribution analysis.
Cost
- Claude API — $0.01-0.05 per invocation (the agent reads files and generates code)
- HubSpot API — included in all plans, no per-call cost
- Slack API — included in all plans
- GitHub Actions (if scheduled) — free tier includes 2,000 minutes/month
Looking to scale your AI operations?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.