Send a weekly Slack report on HubSpot sequence performance using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code, Cursor, or another AI coding agent that supports skills
  • HubSpot Sales Hub Professional or Enterprise (required for Sequences API access)
  • HubSpot private app token stored as an environment variable (HUBSPOT_TOKEN)
  • Slack Bot Token stored as an environment variable (SLACK_BOT_TOKEN)
  • A Slack channel ID for delivery (SLACK_CHANNEL_ID)

Overview

Instead of building a persistent automation, you can create an agent skill — a reusable instruction set that tells your AI coding agent how to pull sequence performance data from HubSpot and post a summary to Slack. This works with Claude Code, and the open Agent Skills standard means the same skill can work across compatible tools.

This approach is ideal for on-demand performance checks and iterating on which metrics to highlight.

Sequences API access

The HubSpot Sequences API is only available with Sales Hub Professional or Enterprise. Verify your plan supports API access before proceeding.

Step 1: Create the skill directory

mkdir -p .claude/skills/sequence-report/scripts

Step 2: Write the SKILL.md file

Create .claude/skills/sequence-report/SKILL.md:

---
name: sequence-report
description: Generates a weekly performance report for HubSpot sequences (open rate, reply rate, meeting rate) and posts it to Slack.
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Generate a weekly sequence performance report by running the bundled script:
 
1. Run: `python $SKILL_DIR/scripts/report.py`
2. Review the output for any errors
3. Confirm the report was posted to Slack

Step 3: Write the report script

Create .claude/skills/sequence-report/scripts/report.py:

#!/usr/bin/env python3
"""
Weekly Sequence Performance Report: HubSpot -> Slack
Fetches sequence enrollment data, calculates open/reply/meeting rates, posts to Slack.
"""
import os
import sys
import time
from datetime import datetime, timezone, timedelta
 
try:
    import requests
except ImportError:
    os.system("pip install requests slack_sdk -q")
    import requests
 
from slack_sdk import WebClient
 
# --- Config ---
HUBSPOT_TOKEN = os.environ.get("HUBSPOT_TOKEN")
SLACK_TOKEN = os.environ.get("SLACK_BOT_TOKEN")
SLACK_CHANNEL = os.environ.get("SLACK_CHANNEL_ID")
 
if not all([HUBSPOT_TOKEN, SLACK_TOKEN, SLACK_CHANNEL]):
    print("ERROR: Missing required env vars: HUBSPOT_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID")
    sys.exit(1)
 
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
 
# --- Date range ---
now = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
start = now - timedelta(days=7)
start_ms = str(int(start.timestamp() * 1000))
 
# --- Fetch sequences ---
print("Fetching sequences...")
seq_resp = requests.get(
    "https://api.hubapi.com/crm/v3/objects/sequences",
    headers=HEADERS,
    params={"limit": 100, "properties": "hs_sequence_name"},
)
seq_resp.raise_for_status()
sequences = seq_resp.json()["results"]
print(f"Found {len(sequences)} sequences")
 
# --- Fetch enrollments per sequence ---
def get_enrollments(seq_id):
    results = []
    after = "0"
    while True:
        resp = requests.post(
            "https://api.hubapi.com/crm/v3/objects/sequence_enrollments/search",
            headers=HEADERS,
            json={
                "filterGroups": [{"filters": [
                    {"propertyName": "hs_sequence_id", "operator": "EQ", "value": str(seq_id)},
                    {"propertyName": "hs_enrollment_start_date", "operator": "GTE", "value": start_ms},
                ]}],
                "properties": [
                    "hs_sequence_id", "hs_enrollment_state",
                    "hs_was_email_opened", "hs_was_email_replied",
                    "hs_was_meeting_booked",
                ],
                "limit": 100,
                "after": after,
            },
        )
        resp.raise_for_status()
        data = resp.json()
        results.extend(data["results"])
        if data.get("paging", {}).get("next", {}).get("after"):
            after = data["paging"]["next"]["after"]
        else:
            break
    return results
 
report = []
for seq in sequences:
    seq_name = seq["properties"].get("hs_sequence_name", f"Sequence {seq['id']}")
    enrollments = get_enrollments(seq["id"])
    time.sleep(0.2)  # Rate limit: 5 req/sec
 
    if not enrollments:
        continue
 
    enrolled = len(enrollments)
    opened = sum(1 for e in enrollments if e["properties"].get("hs_was_email_opened") == "true")
    replied = sum(1 for e in enrollments if e["properties"].get("hs_was_email_replied") == "true")
    meetings = sum(1 for e in enrollments if e["properties"].get("hs_was_meeting_booked") == "true")
 
    report.append({
        "name": seq_name, "enrolled": enrolled, "opened": opened,
        "replied": replied, "meetings": meetings,
        "open_rate": round(opened / enrolled * 100, 1),
        "reply_rate": round(replied / enrolled * 100, 1),
        "meeting_rate": round(meetings / enrolled * 100, 1),
    })
 
report.sort(key=lambda r: r["enrolled"], reverse=True)
 
if not report:
    print("No enrollment data for the last 7 days — skipping.")
    sys.exit(0)
 
# --- Build Slack message ---
total_enrolled = sum(r["enrolled"] for r in report)
total_replied = sum(r["replied"] for r in report)
total_meetings = sum(r["meetings"] for r in report)
 
seq_lines = []
for r in report:
    seq_lines.append(
        f"*{r['name']}* ({r['enrolled']} enrolled)\n"
        f"    Open: {r['open_rate']}% | Reply: {r['reply_rate']}% | Meeting: {r['meeting_rate']}%"
    )
 
blocks = [
    {"type": "header", "text": {"type": "plain_text", "text": "\U0001F4E7 Weekly Sequence Performance Report"}},
    {"type": "section", "fields": [
        {"type": "mrkdwn", "text": f"*Total Enrolled*\n{total_enrolled}"},
        {"type": "mrkdwn", "text": f"*Total Replies*\n{total_replied}"},
        {"type": "mrkdwn", "text": f"*Meetings Booked*\n{total_meetings}"},
    ]},
    {"type": "divider"},
    {"type": "section", "text": {"type": "mrkdwn", "text": "*Per-Sequence Breakdown*\n\n" + "\n\n".join(seq_lines)}},
    {"type": "context", "elements": [
        {"type": "mrkdwn", "text": f"Last 7 days | Generated {datetime.now().strftime('%A, %B %d, %Y')}"}
    ]},
]
 
print("Posting to Slack...")
slack = WebClient(token=SLACK_TOKEN)
result = slack.chat_postMessage(
    channel=SLACK_CHANNEL, text="Weekly Sequence Performance Report",
    blocks=blocks, unfurl_links=False,
)
print(f"Posted report: {result['ts']}")

Step 4: Run the skill

# Claude Code
/sequence-report
 
# Or run directly
python .claude/skills/sequence-report/scripts/report.py

Step 5: Schedule it (optional)

Option A: Claude Desktop Cowork

  1. Open Cowork and go to the Schedule tab
  2. Click + New task
  3. Set description: "Run /sequence-report to post the weekly sequence performance report"
  4. Set frequency to Weekly on Monday mornings
Desktop must be open

Cowork scheduled tasks only run while your computer is awake and the Claude Desktop app is open. If your machine is asleep Monday morning, the task runs when the app next opens.

Option B: Cron + CLI

# crontab -e
0 9 * * 1 cd /path/to/project && python .claude/skills/sequence-report/scripts/report.py

Option C: GitHub Actions

name: Weekly Sequence Report
on:
  schedule:
    - cron: '0 14 * * 1'
  workflow_dispatch: {}
jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install requests slack_sdk
      - run: python .claude/skills/sequence-report/scripts/report.py
        env:
          HUBSPOT_TOKEN: ${{ secrets.HUBSPOT_TOKEN }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
          SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}

When to use this approach

  • You want a sequence report now without setting up n8n or Make
  • You're comparing sequences ad-hoc — "how did the new cold outreach sequence perform this week?"
  • You want to experiment with metrics — quickly add bounce rate or unsubscribe tracking
  • You want the logic version-controlled alongside your code

When to graduate to a dedicated tool

  • You need reliable weekly scheduling without depending on your machine
  • You want visual execution history and automatic retries
  • Multiple sales managers need to configure which sequences to track
Portable skill

Because this skill uses the open Agent Skills standard, the same SKILL.md and script work across Claude Code, Cursor, and other compatible tools. The script itself is just Python — it runs anywhere.

Need help implementing this?

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