Alert Slack when a HubSpot deal is stuck in a stage for over 14 days using code
medium complexityCost: $0
Prerequisites
Prerequisites
- Node.js 18+ or Python 3.9+
- HubSpot private app token
- Slack Bot Token with
chat:writescope - Cron or GitHub Actions for scheduling
Step 1: Build the stale deal finder
import os, requests
from datetime import datetime, timedelta, timezone
from slack_sdk import WebClient
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
# Fetch stage labels
stages_resp = requests.get("https://api.hubapi.com/crm/v3/pipelines/deals",
headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"})
stage_map = {}
for p in stages_resp.json()["results"]:
for s in p["stages"]:
stage_map[s["id"]] = s["label"]
# Search for stale deals
fourteen_days_ago = int((datetime.now(timezone.utc) - timedelta(days=14)).timestamp() * 1000)
resp = requests.post(
"https://api.hubapi.com/crm/v3/objects/deals/search",
headers=HEADERS,
json={
"filterGroups": [{"filters": [
{"propertyName": "hs_lastmodifieddate", "operator": "LT", "value": str(fourteen_days_ago)},
{"propertyName": "dealstage", "operator": "NOT_IN", "values": ["closedwon", "closedlost"]}
]}],
"properties": ["dealname", "amount", "dealstage", "hs_lastmodifieddate"],
"sorts": [{"propertyName": "hs_lastmodifieddate", "direction": "ASCENDING"}],
"limit": 100
}
)
deals = resp.json().get("results", [])
if not deals:
print("No stale deals found")
exit(0)
# Format and send
lines = []
for deal in deals:
props = deal["properties"]
days = (datetime.now(timezone.utc) - datetime.fromisoformat(
props["hs_lastmodifieddate"].replace("Z", "+00:00"))).days
amount = float(props.get("amount") or 0)
stage = stage_map.get(props.get("dealstage", ""), props.get("dealstage", ""))
lines.append(f"• *{props['dealname']}* — {stage} — ${amount:,.0f} — {days}d stale")
slack.chat_postMessage(
channel=os.environ["SLACK_CHANNEL_ID"],
text=f"Stale deals alert: {len(deals)} deals",
blocks=[
{"type": "header", "text": {"type": "plain_text", "text": f"⚠️ {len(deals)} Stale Deals (14+ days)"}},
{"type": "section", "text": {"type": "mrkdwn", "text": "\n".join(lines)}},
{"type": "context", "elements": [{"type": "mrkdwn",
"text": f"Checked {datetime.now().strftime('%A, %B %d, %Y')}"}]}
]
)
print(f"Alerted on {len(deals)} stale deals")Step 2: Schedule
# Daily at 8 AM
0 8 * * * cd /path/to/project && python stale_deals.pyOr use GitHub Actions:
name: Stale Deal Alert
on:
schedule:
- cron: '0 13 * * *' # 8 AM ET
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install requests slack_sdk && python stale_deals.py
env:
HUBSPOT_TOKEN: ${{ secrets.HUBSPOT_TOKEN }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_CHANNEL_ID: ${{ secrets.SLACK_CHANNEL_ID }}Cost
- Free — GitHub Actions provides 2,000 minutes/month on the free tier.
Need help implementing this?
We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.