Flag HubSpot deals with missing fields and Slack the rep using code
Prerequisites
- Node.js 18+ or Python 3.9+
- HubSpot private app token with
crm.objects.deals.readscope - Slack Bot Token (
xoxb-...) withchat:writescope - 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.toolStep 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 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.
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.pyGitHub 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 }}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.