Send a weekly Slack report on HubSpot sequence performance using n8n

high complexityCost: $0-24/mo

Prerequisites

Prerequisites
  • n8n instance — either n8n cloud or self-hosted
  • HubSpot Sales Hub Professional or Enterprise (required for Sequences API access)
  • HubSpot private app token with crm.objects.contacts.read and sales-email-read scopes
  • Slack workspace with a Slack app configured (Bot Token Scopes: chat:write, chat:write.public)
  • n8n credentials set up for both HubSpot and Slack
Sequences API access

The HubSpot Sequences API is only available with Sales Hub Professional or Enterprise. Starter and free plans don't have API access to sequence data, even if sequences are available in the UI.

Step 1: Create the workflow and schedule trigger

Open n8n and create a new workflow. Add a Schedule Trigger node:

  • Trigger interval: Weeks
  • Day of week: Monday
  • Hour: 9
  • Minute: 0
  • Timezone: Set to your team's timezone

Step 2: Fetch all sequences

Add an HTTP Request node to list your sequences:

  • Method: GET
  • URL: https://api.hubapi.com/crm/v3/objects/sequences
  • Authentication: Predefined Credential Type -> HubSpot API
  • Query params: limit=100

This returns each sequence's id, and name (via properties).

Sequences are a custom CRM object

Starting in 2024, HubSpot sequences are accessible via the CRM v3 objects API using the object type sequences. Older guides may reference a legacy /sequences/v2/ endpoint that has been deprecated.

Step 3: Fetch enrollment data for each sequence

Add an HTTP Request node inside a loop to get enrollments per sequence. First, add a Split In Batches node to iterate over sequences, then:

  • Method: POST
  • URL: https://api.hubapi.com/crm/v3/objects/sequence_enrollments/search
  • Body:
{
  "filterGroups": [
    {
      "filters": [
        {
          "propertyName": "hs_sequence_id",
          "operator": "EQ",
          "value": "{{$json.id}}"
        },
        {
          "propertyName": "hs_enrollment_start_date",
          "operator": "GTE",
          "value": "{{$node['Set Dates'].json.seven_days_ago_ms}}"
        }
      ]
    }
  ],
  "properties": [
    "hs_sequence_id", "hs_enrollment_state",
    "hs_was_email_opened", "hs_was_email_replied",
    "hs_was_meeting_booked", "hs_enrollment_start_date"
  ],
  "limit": 100
}
Rate limits with loops

The HubSpot Search API allows 5 requests per second. If you have many sequences, add a Wait node (1 second) inside the loop to stay within limits.

Step 4: Calculate metrics with a Code node

After the loop completes, add a Code node (Run Once for All Items) to aggregate enrollment data:

const sequences = $('Fetch Sequences').first().json.results || [];
const enrollmentBatches = $('Fetch Enrollments').all();
 
// Build sequence name lookup
const seqMap = {};
for (const seq of sequences) {
  seqMap[seq.id] = seq.properties?.hs_sequence_name || `Sequence ${seq.id}`;
}
 
// Aggregate per sequence
const metrics = {};
for (const batch of enrollmentBatches) {
  const enrollments = batch.json.results || [];
  for (const e of enrollments) {
    const seqId = e.properties.hs_sequence_id;
    if (!metrics[seqId]) {
      metrics[seqId] = {
        name: seqMap[seqId] || `Sequence ${seqId}`,
        enrolled: 0,
        opened: 0,
        replied: 0,
        meetingsBooked: 0,
      };
    }
    metrics[seqId].enrolled++;
    if (e.properties.hs_was_email_opened === 'true') metrics[seqId].opened++;
    if (e.properties.hs_was_email_replied === 'true') metrics[seqId].replied++;
    if (e.properties.hs_was_meeting_booked === 'true') metrics[seqId].meetingsBooked++;
  }
}
 
// Calculate rates and format
const report = Object.values(metrics)
  .filter(m => m.enrolled > 0)
  .sort((a, b) => b.enrolled - a.enrolled)
  .map(m => ({
    ...m,
    openRate: (m.opened / m.enrolled * 100).toFixed(1),
    replyRate: (m.replied / m.enrolled * 100).toFixed(1),
    meetingRate: (m.meetingsBooked / m.enrolled * 100).toFixed(1),
  }));
 
const totalEnrolled = report.reduce((s, r) => s + r.enrolled, 0);
const totalReplied = report.reduce((s, r) => s + r.replied, 0);
const totalMeetings = report.reduce((s, r) => s + r.meetingsBooked, 0);
 
return [{
  json: { report, totalEnrolled, totalReplied, totalMeetings }
}];

Step 5: Format and send to Slack

Add a Slack node:

  • Resource: Message
  • Operation: Send a Message
  • Channel: #sales-reports
  • Message Type: Block Kit
{
  "blocks": [
    {
      "type": "header",
      "text": {
        "type": "plain_text",
        "text": "📧 Weekly Sequence Performance Report"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Total Enrolled*\n{{ $json.totalEnrolled }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Total Replies*\n{{ $json.totalReplied }}"
        },
        {
          "type": "mrkdwn",
          "text": "*Meetings Booked*\n{{ $json.totalMeetings }}"
        }
      ]
    },
    {
      "type": "divider"
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Per-Sequence Breakdown*\n{{ $json.report.map(r => `*${r.name}* (${r.enrolled} enrolled)\n    Open: ${r.openRate}% | Reply: ${r.replyRate}% | Meeting: ${r.meetingRate}%`).join('\\n\\n') }}"
      }
    },
    {
      "type": "context",
      "elements": [
        {
          "type": "mrkdwn",
          "text": "Last 7 days | Generated {{ new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }) }}"
        }
      ]
    }
  ]
}
Block Kit text limit

Section text has a 3,000-character max. If you have many sequences, truncate the list to the top 10 and add a "and N more" note, or split across multiple blocks.

Step 6: Add error handling and activate

  1. Enable Settings -> Retry On Fail on each HTTP Request node (2 retries, 5-second wait)
  2. Create an Error Workflow with an Error Trigger that sends a Slack DM on failure
  3. Click Execute Workflow to test
  4. Toggle the workflow to Active

Cost

  • n8n Cloud Starter: $24/mo for 2,500 executions. A weekly report with 10 sequences uses ~1 execution (the loop runs within a single execution). Self-hosted n8n is free.
  • Maintenance: monitor for new sequences in HubSpot. The workflow fetches all sequences dynamically, so new ones appear automatically.

Next steps

  • Step-level analytics — extend the enrollment query to include step-level data for a more granular view of which email steps perform best
  • Week-over-week comparison — store last week's metrics in n8n static data and show trend arrows
  • Threshold alerts — add an IF node to flag sequences with reply rates below 5% and tag them with a warning emoji

Need help implementing this?

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