Flag HubSpot deals with missing fields and Slack the rep using code

medium complexityCost: $0

Prerequisites

Prerequisites
  • Node.js 18+ or Python 3.9+
  • HubSpot private app token with crm.objects.deals.read scope
  • Slack Bot Token (xoxb-...) with chat:write scope
  • A mapping of HubSpot owner IDs to Slack user IDs
  • Cron, GitHub Actions, or a cloud function for scheduling

Why code?

A script gives you full control over which fields to check, how to format the Slack messages, and how to handle edge cases — all with zero ongoing cost. The daily audit runs in under 10 seconds, making it perfect for cron or GitHub Actions. The trade-off is you handle the owner-to-Slack mapping and error monitoring yourself. Best for teams with a developer who can maintain the script.

How it works

  • Cron or GitHub Actions triggers the script daily at 7 AM
  • HubSpot Search API runs one query per required field using NOT_HAS_PROPERTY
  • Deduplication logic merges results and identifies all missing fields per deal
  • Grouping organizes deals by owner for consolidated messages
  • Slack SDK sends a Block Kit DM to each owner with their deals and HubSpot links

Step 1: Set up the project

# Verify your HubSpot token works
curl -s -X POST "https://api.hubapi.com/crm/v3/objects/deals/search" \
  -H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filterGroups": [{
      "filters": [{
        "propertyName": "closedate",
        "operator": "NOT_HAS_PROPERTY"
      }, {
        "propertyName": "dealstage",
        "operator": "NOT_IN",
        "values": ["closedwon", "closedlost"]
      }]
    }],
    "properties": ["dealname", "amount", "closedate", "hubspot_owner_id"],
    "limit": 5
  }' | python3 -m json.tool

Step 2: Search for deals with missing fields

Use the HubSpot Search API with NOT_HAS_PROPERTY to find deals where closedate or amount is null. Two separate searches are needed because HubSpot applies AND logic within a filter group.

# Search for deals missing close date
curl -s -X POST "https://api.hubapi.com/crm/v3/objects/deals/search" \
  -H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filterGroups": [{
      "filters": [
        {"propertyName": "closedate", "operator": "NOT_HAS_PROPERTY"},
        {"propertyName": "dealstage", "operator": "NOT_IN", "values": ["closedwon", "closedlost"]}
      ]
    }],
    "properties": ["dealname", "amount", "closedate", "dealstage", "hubspot_owner_id"],
    "limit": 100
  }'
 
# Search for deals missing amount
curl -s -X POST "https://api.hubapi.com/crm/v3/objects/deals/search" \
  -H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filterGroups": [{
      "filters": [
        {"propertyName": "amount", "operator": "NOT_HAS_PROPERTY"},
        {"propertyName": "dealstage", "operator": "NOT_IN", "values": ["closedwon", "closedlost"]}
      ]
    }],
    "properties": ["dealname", "amount", "closedate", "dealstage", "hubspot_owner_id"],
    "limit": 100
  }'
NOT_HAS_PROPERTY vs empty string

NOT_HAS_PROPERTY matches deals where the property has never been set. If a rep sets a close date and then clears it, HubSpot may store it as an empty string rather than null. To catch both, add a second filter group with EQ operator and value: "" for each field.

Pagination

The search endpoint returns max 100 results per page. If you have more than 100 deals with missing fields, implement pagination using the after cursor from paging.next.after in the response. The endpoint caps at 10,000 total results.

Step 3: Schedule

Cron (server-based):

# Daily at 7 AM
0 7 * * * cd /path/to/missing-fields && python missing_fields.py

GitHub Actions (serverless):

name: Missing Deal Fields Alert
on:
  schedule:
    - cron: '0 12 * * *'  # 7 AM ET = 12 PM UTC
  workflow_dispatch: {}
jobs:
  alert:
    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 && python missing_fields.py
        env:
          HUBSPOT_ACCESS_TOKEN: ${{ secrets.HUBSPOT_ACCESS_TOKEN }}
          HUBSPOT_PORTAL_ID: ${{ secrets.HUBSPOT_PORTAL_ID }}
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Environment variables

Never commit tokens to your repo. Use GitHub Secrets, .env files (gitignored), or your hosting platform's secrets manager.

Troubleshooting

Common questions

What's the difference between NOT_HAS_PROPERTY and checking for empty strings?

NOT_HAS_PROPERTY matches properties that have never been set. If a rep enters a value and then clears it, HubSpot may store an empty string instead of null. To catch both, add a second filter group with EQ operator and an empty string value, then merge the results.

How do I find my HubSpot owner IDs?

Call GET https://api.hubapi.com/crm/v3/owners with your token. Each owner has a numeric id field — use that as the key in your owner-to-Slack mapping.

Can I check fields like next step, competitor, or contact role?

Yes. Add the field's internal name to the REQUIRED_FIELDS list and include it in the PROPERTIES array. The deduplication logic handles any number of fields automatically.

What happens if a deal has no owner?

Deals without an owner get grouped under the "unassigned" key. The script logs these but skips the Slack DM since there's no one to notify. You can add a fallback channel for unassigned deals if needed.

Cost

  • Free -- GitHub Actions provides 2,000 minutes/month on the free tier. This script runs in under 10 seconds.

Looking to scale your AI operations?

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