Post a daily Slack leaderboard of rep activity from HubSpot using n8n
Prerequisites
- n8n instance — either n8n cloud or self-hosted
- HubSpot private app token with
crm.objects.engagements.readandcrm.objects.owners.readscopes - Slack workspace with a Slack app configured (Bot Token Scopes:
chat:write,chat:write.public) - n8n credentials set up for both HubSpot and Slack
Step 1: Create the workflow and schedule trigger
Open n8n and create a new workflow. Add a Schedule Trigger node:
- Trigger interval: Days
- Days between triggers: 1
- Hour: 8
- Minute: 0
- Timezone: Set to your team's timezone
This fires every weekday morning (you'll add a weekday filter in the next step).
Step 2: Filter to weekdays only
Add an IF node to skip weekends:
// Expression in the IF node condition
{{ new Date().getDay() >= 1 && new Date().getDay() <= 5 }}Route the true output to the rest of the workflow. The false output goes nowhere — the workflow ends silently on weekends.
n8n cloud defaults to UTC. Set the timezone in the Schedule Trigger node — not the workflow settings — so the day-of-week check uses your local time.
Step 3: Fetch owner list from HubSpot
Add an HTTP Request node to get your rep roster:
- Method: GET
- URL:
https://api.hubapi.com/crm/v3/owners - Authentication: Predefined Credential Type -> HubSpot API
- Query params:
limit=100
This returns each owner's id, firstName, lastName, and email. You'll use the id to match against engagement owners.
Step 4: Fetch yesterday's calls
Add an HTTP Request node:
- Method: POST
- URL:
https://api.hubapi.com/crm/v3/objects/calls/search - Body content type: JSON
- Body:
{
"filterGroups": [
{
"filters": [
{
"propertyName": "hs_timestamp",
"operator": "GTE",
"value": "{{yesterday_start_ms}}"
},
{
"propertyName": "hs_timestamp",
"operator": "LT",
"value": "{{today_start_ms}}"
}
]
}
],
"properties": ["hs_timestamp", "hubspot_owner_id"],
"limit": 100
}Replace the timestamp expressions with n8n expressions. In a Set node before this step, compute:
// yesterday start (midnight) in milliseconds
{{ new Date(new Date().setHours(0,0,0,0) - 86400000).getTime() }}
// today start (midnight) in milliseconds
{{ new Date(new Date().setHours(0,0,0,0)).getTime() }}HubSpot's hs_timestamp filter expects Unix milliseconds as a string, not an ISO date. The Search API will silently return zero results if you pass seconds instead of milliseconds.
Step 5: Fetch yesterday's emails and meetings
Duplicate the calls HTTP Request node twice — once for emails and once for meetings:
Emails:
- URL:
https://api.hubapi.com/crm/v3/objects/emails/search - Same filter body, same properties
Meetings:
- URL:
https://api.hubapi.com/crm/v3/objects/meetings/search - Same filter body, same properties
In n8n, you can run these three HTTP Request nodes in parallel by connecting them all to the same Set node output. n8n executes sibling branches concurrently, cutting execution time.
Step 6: Merge and rank reps with a Code node
Add a Merge node (mode: Append) to combine all three activity streams into one. Then add a Code node (Run Once for All Items):
const owners = $('Fetch Owners').first().json.results;
const calls = $('Fetch Calls').first().json.results || [];
const emails = $('Fetch Emails').first().json.results || [];
const meetings = $('Fetch Meetings').first().json.results || [];
// Build owner name lookup
const ownerMap = {};
for (const o of owners) {
ownerMap[o.id] = `${o.firstName} ${o.lastName}`.trim() || o.email;
}
// Count activities per owner
const reps = {};
function count(items, type) {
for (const item of items) {
const ownerId = item.properties.hubspot_owner_id;
if (!ownerId) continue;
if (!reps[ownerId]) reps[ownerId] = { calls: 0, emails: 0, meetings: 0, total: 0 };
reps[ownerId][type]++;
reps[ownerId].total++;
}
}
count(calls, 'calls');
count(emails, 'emails');
count(meetings, 'meetings');
// Rank by total activity
const ranked = Object.entries(reps)
.map(([id, counts]) => ({
name: ownerMap[id] || `Owner ${id}`,
...counts
}))
.sort((a, b) => b.total - a.total);
// Assign medals
const medals = ['🥇', '🥈', '🥉'];
const leaderboard = ranked.map((rep, i) => ({
rank: i + 1,
medal: medals[i] || `${i + 1}.`,
...rep
}));
return [{ json: { leaderboard, date: new Date(Date.now() - 86400000).toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' }) } }];Step 7: Format and send to Slack
Add a Slack node:
- Resource: Message
- Operation: Send a Message
- Channel: Select your
#sales-activitychannel - Message Type: Block Kit
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🏆 Rep Activity Leaderboard"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Activity for {{ $json.date }}"
}
]
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "{{ $json.leaderboard.map(r => `${r.medal} *${r.name}* — ${r.total} activities (${r.calls}C ${r.emails}E ${r.meetings}M)`).join('\\n') }}"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "C = Calls | E = Emails | M = Meetings"
}
]
}
]
}n8n's Slack node expects the full {"blocks": [...]} wrapper. If you pass just the array, n8n silently falls back to the notification text field.
Step 8: Add error handling and activate
- Enable Settings -> Retry On Fail on each HTTP Request node (2 retries, 5-second wait)
- Create an Error Workflow with an Error Trigger that sends you a Slack DM on failure
- Click Execute Workflow to test with yesterday's data
- Toggle the workflow to Active
Cost
- n8n Cloud Starter: $24/mo for 2,500 executions. A daily report uses ~20 executions/month (weekdays only). Self-hosted n8n is free.
- Maintenance: Update owner IDs if reps join or leave. The owner list is fetched dynamically, so new reps appear automatically.
Next steps
- Weighted scoring — assign points (1 for email, 2 for call, 3 for meeting) instead of raw counts to reward high-effort activities
- Channel-specific boards — post SDR leaderboard to
#sdr-teamand AE leaderboard to#ae-teamusing a Router node - Week-over-week streak — use n8n static data (
$getWorkflowStaticData('global')) to track who held the top spot multiple days in a row
Need help implementing this?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.