Round-robin route HubSpot leads and notify reps in Slack using n8n
Prerequisites
- 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
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
Cost
- n8n Cloud Starter: $24/mo for 2,500 executions. Each new lead = 1 execution.
- Self-hosted: Free. Unlimited executions.
Need help implementing this?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.