Flag HubSpot deals with missing fields and Slack the rep using an agent skill

low complexityCost: Usage-based

Prerequisites

Prerequisites
  • Claude Code or compatible agent
  • Environment variables: HUBSPOT_TOKEN, HUBSPOT_PORTAL_ID, SLACK_BOT_TOKEN
  • A JSON file or environment variable mapping HubSpot owner IDs to Slack user IDs

Step 1: Create the skill

Create .claude/skills/missing-deal-fields/SKILL.md:

---
name: missing-deal-fields
description: Find HubSpot deals missing close date or amount and DM the deal owner in Slack
disable-model-invocation: true
allowed-tools: Bash(python *)
---
 
Audit active deal data quality. Searches for deals missing required fields
(close date, amount) and sends each deal owner a Slack DM listing their
incomplete deals with links to fix them.
 
Run: `python $SKILL_DIR/scripts/audit.py`

Step 2: Write the audit script

Create .claude/skills/missing-deal-fields/scripts/audit.py:

import os, json, requests
from slack_sdk import WebClient
 
HUBSPOT_TOKEN = os.environ["HUBSPOT_TOKEN"]
PORTAL_ID = os.environ.get("HUBSPOT_PORTAL_ID", "YOUR_PORTAL_ID")
HEADERS = {"Authorization": f"Bearer {HUBSPOT_TOKEN}", "Content-Type": "application/json"}
SEARCH_URL = "https://api.hubapi.com/crm/v3/objects/deals/search"
 
slack = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
 
# Load owner-to-Slack mapping from env or file
OWNER_MAP_PATH = os.path.join(os.path.dirname(__file__), "owner_map.json")
if os.path.exists(OWNER_MAP_PATH):
    with open(OWNER_MAP_PATH) as f:
        OWNER_TO_SLACK = json.load(f)
else:
    OWNER_TO_SLACK = json.loads(os.environ.get("OWNER_TO_SLACK", "{}"))
 
REQUIRED_FIELDS = ["closedate", "amount"]
PROPERTIES = ["dealname", "amount", "closedate", "dealstage", "hubspot_owner_id"]
 
 
def search_missing(field_name):
    resp = requests.post(SEARCH_URL, headers=HEADERS, json={
        "filterGroups": [{"filters": [
            {"propertyName": field_name, "operator": "NOT_HAS_PROPERTY"},
            {"propertyName": "dealstage", "operator": "NOT_IN",
             "values": ["closedwon", "closedlost"]},
        ]}],
        "properties": PROPERTIES,
        "limit": 100,
    })
    resp.raise_for_status()
    return resp.json().get("results", [])
 
 
# Fetch and deduplicate
seen = set()
all_deals = []
 
for field in REQUIRED_FIELDS:
    for deal in search_missing(field):
        if deal["id"] not in seen:
            seen.add(deal["id"])
            props = deal["properties"]
            missing = [f for f in REQUIRED_FIELDS if not props.get(f)]
            all_deals.append({
                "id": deal["id"],
                "name": props["dealname"],
                "owner_id": props.get("hubspot_owner_id") or "unassigned",
                "missing": missing,
            })
 
if not all_deals:
    print("All active deals have complete fields.")
    exit(0)
 
# Group by owner
by_owner = {}
for deal in all_deals:
    by_owner.setdefault(deal["owner_id"], []).append(deal)
 
# Notify each owner
notified = 0
for owner_id, deals in by_owner.items():
    slack_user = OWNER_TO_SLACK.get(owner_id)
    if not slack_user:
        print(f"No Slack mapping for owner {owner_id} ({len(deals)} deals)")
        continue
 
    lines = []
    for d in deals:
        link = f"https://app.hubspot.com/contacts/{PORTAL_ID}/deal/{d['id']}"
        lines.append(f"- <{link}|{d['name']}> -- missing *{', '.join(d['missing'])}*")
 
    slack.chat_postMessage(
        channel=slack_user,
        text=f"Missing deal fields: {len(deals)} deals",
        blocks=[
            {"type": "header", "text": {"type": "plain_text",
                "text": f"Missing Deal Fields ({len(deals)} deals)"}},
            {"type": "section", "text": {"type": "mrkdwn",
                "text": "\n".join(lines)}},
            {"type": "context", "elements": [{"type": "mrkdwn",
                "text": "Please update these deals today so forecasting stays accurate."}]},
        ],
    )
    notified += 1
    print(f"Notified {slack_user} about {len(deals)} deals")
 
print(f"Done. {len(all_deals)} deals flagged, {notified} owners notified.")

Step 3: Add the owner mapping

Create .claude/skills/missing-deal-fields/scripts/owner_map.json:

{
  "12345678": "U0XXXXXXXXX",
  "87654321": "U0YYYYYYYYY"
}

Replace the keys with your HubSpot owner IDs and the values with the corresponding Slack user IDs.

Step 4: Run

/missing-deal-fields

When to use this

  • Before pipeline review meetings to make sure every deal has the basics filled in
  • As a weekly hygiene check when forecast accuracy drops
  • After importing or migrating deals to catch incomplete records
  • For managers who want to audit data quality on-demand without waiting for a scheduled job

Need help implementing this?

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