Post a daily Slack leaderboard of rep activity from HubSpot using Zapier

medium complexityCost: $20-50/mo

Prerequisites

Prerequisites
  • Zapier account on the Professional plan or higher (required for multi-step Zaps, Code by Zapier, and Webhooks by Zapier)
  • HubSpot private app token with engagement scopes
  • Slack workspace connected to Zapier

Overview

Zapier's built-in HubSpot integration doesn't have a "list engagements" action. To build a leaderboard, you'll use Schedule by Zapier to trigger daily, Webhooks by Zapier to call HubSpot's Engagements API directly, Code by Zapier to rank reps, and Slack to post the result.

Why Webhooks instead of built-in HubSpot actions?

Zapier's native HubSpot actions work on individual records (triggers like "New Engagement"). For a daily aggregated leaderboard, you need to search all of yesterday's activities — which requires calling the HubSpot Search API directly via Webhooks by Zapier.

Step 1: Add a Schedule trigger

Create a new Zap. Choose Schedule by Zapier:

  • Trigger event: Every Day
  • Time of day: 8:00am
  • Day of the week: Choose Monday through Friday only (skip weekends)
  • Timezone: Select your team's timezone

Step 2: Fetch rep owners via Webhooks

Add a Webhooks by Zapier action:

  • Action event: Custom Request
  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/owners?limit=100
  • Headers: Authorization: Bearer YOUR_HUBSPOT_PRIVATE_APP_TOKEN

This returns your rep roster with IDs and names for mapping later.

Step 3: Fetch yesterday's calls

Add another Webhooks by Zapier action:

  • Method: POST
  • URL: https://api.hubapi.com/crm/v3/objects/calls/search
  • Headers:
    • Authorization: Bearer YOUR_HUBSPOT_PRIVATE_APP_TOKEN
    • Content-Type: application/json
  • Data (raw JSON):
{
  "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 YESTERDAY_START_MS and TODAY_START_MS with Zapier formatter steps or compute them in the Code step.

Step 4: Fetch emails and meetings

Repeat Step 3 twice with different URLs:

  • Emails: https://api.hubapi.com/crm/v3/objects/emails/search
  • Meetings: https://api.hubapi.com/crm/v3/objects/meetings/search

Use the same filter body for both.

Zap step limits

Each Webhook call is a separate Zap step and counts toward your task usage. This Zap uses 6+ steps per run (schedule + owners + 3 searches + code + Slack). On the Professional plan at $29.99/mo, you get 750 tasks/month. Daily weekday runs use ~140 tasks/month.

Step 5: Rank reps with Code by Zapier

Add a Code by Zapier step (JavaScript). Map the raw responses from the three Webhook steps into input variables:

  • ownersRaw -> response body from the owners Webhook
  • callsRaw -> response body from the calls Webhook
  • emailsRaw -> response body from the emails Webhook
  • meetingsRaw -> response body from the meetings Webhook
const owners = JSON.parse(inputData.ownersRaw).results || [];
const calls = JSON.parse(inputData.callsRaw).results || [];
const emails = JSON.parse(inputData.emailsRaw).results || [];
const meetings = JSON.parse(inputData.meetingsRaw).results || [];
 
const ownerMap = {};
for (const o of owners) {
  ownerMap[o.id] = `${o.firstName || ''} ${o.lastName || ''}`.trim() || o.email;
}
 
const reps = {};
function count(items, type) {
  for (const item of items) {
    const oid = item.properties.hubspot_owner_id;
    if (!oid) continue;
    if (!reps[oid]) reps[oid] = { calls: 0, emails: 0, meetings: 0, total: 0 };
    reps[oid][type]++;
    reps[oid].total++;
  }
}
 
count(calls, 'calls');
count(emails, 'emails');
count(meetings, 'meetings');
 
const medals = ['\u{1F947}', '\u{1F948}', '\u{1F949}'];
const ranked = Object.entries(reps)
  .map(([id, c]) => ({ name: ownerMap[id] || id, ...c }))
  .sort((a, b) => b.total - a.total);
 
const lines = ranked.map((r, i) => {
  const medal = medals[i] || `${i + 1}.`;
  return `${medal} *${r.name}* — ${r.total} activities (${r.calls}C ${r.emails}E ${r.meetings}M)`;
});
 
const yesterday = new Date(Date.now() - 86400000);
return {
  leaderboard: lines.join('\n'),
  reportDate: yesterday.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
};

Step 6: Send to Slack

Add a Slack action:

  • Action event: Send Channel Message
  • Channel: #sales-activity
  • Message Text:
:trophy: *Rep Activity Leaderboard*
Activity for {{reportDate}}
 
{{leaderboard}}
 
_C = Calls | E = Emails | M = Meetings_

Step 7: Test and publish

  1. Click Test on each step to verify data flows correctly
  2. Review the Slack message — check that rep names resolve and counts look right
  3. Turn the Zap On

Cost and task usage

  • Professional plan: $29.99/mo (billed annually) with 750 tasks/month
  • This Zap uses ~7 tasks per run: 1 schedule + 1 owners + 3 searches + 1 code + 1 Slack = 7 steps. At 20 weekday runs/month, that's ~140 tasks/month.
  • If you have more than 100 activities of any type per day, you'll need additional Webhook steps for pagination, increasing task usage.

Limitations

  • No native pagination: If your team logs more than 100 calls, emails, or meetings per day, you'll only capture the first 100 of each type. You'd need additional Webhook steps with after cursors.
  • Code step sandboxing: Code by Zapier can't make HTTP requests — all data must be passed in via Input Data.
  • Timestamp computation: Computing yesterday's midnight in milliseconds requires either a Formatter step or inline calculation in the Code step.

Need help implementing this?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.