Send a Slack alert when a HubSpot deal changes stage using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code or another agent that supports the Agent Skills standard
  • HubSpot private app token stored as HUBSPOT_TOKEN environment variable
  • Slack Bot Token stored as SLACK_BOT_TOKEN environment variable
  • Slack channel ID stored as SLACK_CHANNEL_ID environment variable

Overview

This approach creates an agent skill that checks for recent deal stage changes and posts alerts to Slack. Unlike the webhook-based approaches, this runs on-demand or on a schedule — ideal for a periodic check rather than real-time alerts.

Step 1: Create the skill

Create .claude/skills/deal-stage-alerts/SKILL.md:

---
name: deal-stage-alerts
description: Check for recent HubSpot deal stage changes and post alerts to Slack
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Check for HubSpot deals that changed stage in the last hour and post alerts to Slack.
 
Run: `python $SKILL_DIR/scripts/check_stages.py`

Step 2: Write the script

Create .claude/skills/deal-stage-alerts/scripts/check_stages.py:

#!/usr/bin/env python3
import os, sys, requests
from datetime import datetime, timedelta, timezone
 
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: Set HUBSPOT_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID")
    sys.exit(1)
 
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
 
# Get pipeline stages
stages_resp = requests.get("https://api.hubapi.com/crm/v3/pipelines/deals", headers=HEADERS)
stages_resp.raise_for_status()
stage_map = {}
for p in stages_resp.json()["results"]:
    for s in p["stages"]:
        stage_map[s["id"]] = s["label"]
 
# Find recently modified deals
one_hour_ago = int((datetime.now(timezone.utc) - timedelta(hours=1)).timestamp() * 1000)
resp = requests.post(
    "https://api.hubapi.com/crm/v3/objects/deals/search",
    headers=HEADERS,
    json={
        "filterGroups": [{"filters": [
            {"propertyName": "hs_lastmodifieddate", "operator": "GTE", "value": str(one_hour_ago)}
        ]}],
        "properties": ["dealname", "amount", "dealstage"],
        "limit": 100
    }
)
resp.raise_for_status()
deals = resp.json().get("results", [])
 
if not deals:
    print("No deals modified in the last hour")
    sys.exit(0)
 
# Post to Slack
from slack_sdk import WebClient
slack = WebClient(token=SLACK_TOKEN)
 
for deal in deals:
    props = deal["properties"]
    stage_name = stage_map.get(props.get("dealstage", ""), props.get("dealstage", "Unknown"))
    amount = float(props.get("amount") or 0)
 
    slack.chat_postMessage(
        channel=SLACK_CHANNEL,
        text=f"Deal updated: {props['dealname']}",
        blocks=[
            {"type": "section", "text": {"type": "mrkdwn",
                "text": f"🔄 *Deal Stage Changed*\n*{props['dealname']}* is in *{stage_name}*\nAmount: ${amount:,.0f}"}},
            {"type": "context", "elements": [{"type": "mrkdwn",
                "text": f"<https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{deal['id']}|View in HubSpot>"}]}
        ]
    )
 
print(f"Posted {len(deals)} deal alerts to Slack")

Step 3: Run it

/deal-stage-alerts

Step 4: Schedule (optional)

For hourly checks, schedule via Cowork or cron:

# crontab — run every hour
0 * * * * cd /path/to/project && python .claude/skills/deal-stage-alerts/scripts/check_stages.py

When to use this approach

  • You want periodic digest-style alerts, not real-time
  • You don't want to maintain a webhook server
  • You want to run checks on demand during pipeline reviews

Need help implementing this?

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