Send a weekly Slack report on HubSpot sequence performance using n8n
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.readandsales-email-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
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).
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
}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' }) }}"
}
]
}
]
}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
- 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 a Slack DM on failure
- Click Execute Workflow to test
- 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.