Round-robin route HubSpot leads and notify reps in Slack using n8n
Install this workflow
Download the n8n workflow JSON and import it into your n8n instance.
round-robin.n8n.jsonPrerequisites
- n8n instance (cloud or self-hosted)
- HubSpot private app token with
crm.objects.contacts.read,crm.objects.contacts.write, andsettings.users.readscopes - Slack app with Bot Token (
chat:write,users:readscopes) - n8n credentials configured for both HubSpot and Slack
- A mapping of HubSpot owner IDs to Slack user IDs
Why n8n?
n8n is the best option for round-robin routing because its static data feature ($getWorkflowStaticData) persists the counter across executions without external storage. The Code node handles the assignment logic in a single step, and the visual builder makes it easy to add filters (skip already-assigned contacts) or branches (different routing for different lead sources).
Self-hosted n8n is free with unlimited executions — important when every new lead triggers the workflow regardless of volume.
How it works
- HubSpot Trigger fires on contact creation, then an IF node filters out already-assigned contacts
- HTTP Request fetches the full contact record with enrichment data
- Code node reads the round-robin counter from static data, selects the next rep, and increments
- HTTP Request updates the contact's
hubspot_owner_idin HubSpot - Slack node sends a Block Kit DM to the assigned rep
Step 1: Add a HubSpot Trigger node
Create a new workflow and add a HubSpot Trigger node:
- Authentication: Select your HubSpot credential
- Event: Contact Created
This fires every time a new contact is created in HubSpot. The payload includes the contact ID.
Step 2: Fetch contact details
Add an HTTP Request node:
- Method: GET
- URL:
https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.objectId }} - Authentication: Predefined -> HubSpot API
- Query params:
properties=firstname,lastname,email,company,jobtitle,hubspot_owner_id
If the contact already has a hubspot_owner_id, it was assigned by another workflow or manually. Add an IF node to check $json.properties.hubspot_owner_id is empty before proceeding.
Step 3: Round-robin assignment with a Code node
Add a Code node. n8n's static data persists across executions, making it perfect for tracking the round-robin counter:
// Rep roster: HubSpot owner ID → Slack user ID
const reps = [
{ name: "Alice", hubspotOwnerId: "12345678", slackUserId: "U01AAAA" },
{ name: "Bob", hubspotOwnerId: "23456789", slackUserId: "U02BBBB" },
{ name: "Carol", hubspotOwnerId: "34567890", slackUserId: "U03CCCC" },
{ name: "Dave", hubspotOwnerId: "45678901", slackUserId: "U04DDDD" },
];
// Get and increment the counter using n8n static data
const staticData = $getWorkflowStaticData('global');
const currentIndex = staticData.roundRobinIndex || 0;
const assignedRep = reps[currentIndex % reps.length];
staticData.roundRobinIndex = (currentIndex + 1) % reps.length;
const contact = $('Fetch Contact').first().json;
return [{
json: {
contactId: contact.id,
contactName: `${contact.properties.firstname || ''} ${contact.properties.lastname || ''}`.trim(),
contactEmail: contact.properties.email,
company: contact.properties.company,
title: contact.properties.jobtitle,
assignedRep: assignedRep,
}
}];n8n's $getWorkflowStaticData('global') survives across executions but resets if you re-deploy the workflow from scratch. If the counter resets, the worst case is one round of uneven distribution before it balances out.
Step 4: Update contact owner in HubSpot
Add an HTTP Request node:
- Method: PATCH
- URL:
https://api.hubapi.com/crm/v3/objects/contacts/{{ $json.contactId }} - Body:
{
"properties": {
"hubspot_owner_id": "{{ $json.assignedRep.hubspotOwnerId }}"
}
}Step 5: Send Slack DM to the assigned rep
Add a Slack node:
- Resource: Message
- Operation: Send
- Channel:
{{ $json.assignedRep.slackUserId }}(DM by user ID) - Message Type: Block Kit
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🆕 *New Lead Assigned to You*\n*{{ $json.contactName }}* — {{ $json.title || 'No title' }} at {{ $json.company || 'Unknown company' }}\nEmail: {{ $json.contactEmail }}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": { "type": "plain_text", "text": "View in HubSpot" },
"url": "https://app.hubspot.com/contacts/YOUR_PORTAL_ID/contact/{{ $json.contactId }}"
}
]
}
]
}To send a Slack DM, use the user ID (e.g., U01AAAA) as the channel value. The bot must have chat:write scope and the user must be in the workspace.
Step 6: Activate
- Click Execute Workflow to test with a new contact
- Verify the contact's owner was set in HubSpot and the Slack DM arrived
- Toggle the workflow to Active
Troubleshooting
Common questions
What happens to the counter if the workflow is re-deployed?
n8n's $getWorkflowStaticData('global') survives across executions but resets if you delete and recreate the workflow from scratch. If it resets, the worst case is one round of slightly uneven distribution before it balances out. For critical fairness guarantees, store the counter in a database or Redis instead.
How do I skip reps who are out of office?
Add an activeReps filter in the Code node. Maintain a Google Sheet or n8n credential variable with the list of available rep indices. The Code node reads this list and only assigns to active reps. When someone returns, update the list — no workflow changes needed.
Can I do weighted routing instead of equal round-robin?
Yes. Replace the simple modulo counter with a weighted distribution. For example, give senior reps 2 slots in the rotation array (e.g., [Alice, Alice, Bob, Carol]) so they receive proportionally more leads.
Cost
- n8n Cloud Starter: $24/mo for 2,500 executions. Each new lead = 1 execution.
- Self-hosted: Free. Unlimited executions.
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.