Flag HubSpot deals with missing fields and Slack the rep using n8n
Install this workflow
Download the n8n workflow JSON and import it into your n8n instance.
missing-fields.n8n.jsonPrerequisites
- n8n instance (cloud or self-hosted)
- HubSpot private app token with
crm.objects.deals.readscope - Slack app with Bot Token (
chat:writescope) - n8n credentials for HubSpot and Slack
- A mapping of HubSpot owner IDs to Slack user IDs (Google Sheet or static data)
Why n8n?
n8n is the best option for missing field alerts because the Code node handles deduplication, grouping by owner, and message formatting in a few lines of JavaScript. The visual workflow makes it easy to add more required fields or adjust the Slack message format. Self-hosted n8n runs this for free, and even cloud n8n uses just 30 executions per month for a daily check.
How it works
- Schedule Trigger fires daily at 7 AM
- HTTP Request nodes search for deals missing close date and amount separately (HubSpot requires separate queries)
- Code node deduplicates results, identifies all missing fields per deal, and groups deals by owner
- Slack node DMs each owner with their incomplete deals and direct HubSpot links
Step 1: Schedule a daily check
Add a Schedule Trigger node:
- Trigger interval: Days
- Trigger at hour: 7 (run before the team starts their day)
Step 2: Search for deals missing close date
Add an HTTP Request node named "Search Missing Close Date":
- Method: POST
- URL:
https://api.hubapi.com/crm/v3/objects/deals/search - Authentication: HubSpot API credential
- Body:
{
"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
}Step 3: Search for deals missing amount
Add a second HTTP Request node named "Search Missing Amount" with the same structure, but swap the filter:
{
"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
}HubSpot's Search API applies AND logic within a filter group. You cannot OR two NOT_HAS_PROPERTY filters in a single group. Running two searches and merging results is the reliable approach.
Step 4: Merge and group by owner
Add a Code node to deduplicate and group deals by owner:
const missingClose = $('Search Missing Close Date').first().json.results || [];
const missingAmount = $('Search Missing Amount').first().json.results || [];
// Deduplicate by deal ID
const seen = new Set();
const allDeals = [];
for (const deal of [...missingClose, ...missingAmount]) {
if (!seen.has(deal.id)) {
seen.add(deal.id);
const props = deal.properties;
const missing = [];
if (!props.closedate) missing.push('close date');
if (!props.amount) missing.push('amount');
allDeals.push({
id: deal.id,
name: props.dealname,
stage: props.dealstage,
ownerId: props.hubspot_owner_id || 'unassigned',
missing,
});
}
}
const byOwner = {};
for (const deal of allDeals) {
if (!byOwner[deal.ownerId]) byOwner[deal.ownerId] = [];
byOwner[deal.ownerId].push(deal);
}
return Object.entries(byOwner).map(([ownerId, deals]) => ({
json: { ownerId, deals, dealCount: deals.length }
}));Step 5: Slack DM each rep
Add a Slack node (executes once per owner from the Code node output):
- Operation: Send a message
- Channel: DM the owner using their Slack user ID from your mapping
- Message Type: Block Kit
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Missing Deal Fields"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "You have *{{ $json.dealCount }}* deals with missing fields:\n\n{{ $json.deals.map(d => `- <https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/${d.id}|${d.name}> -- missing *${d.missing.join(', ')}*`).join('\\n') }}"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Please update these deals today so forecasting stays accurate."
}
]
}
]
}To DM individual reps, you need a mapping of HubSpot owner IDs to Slack user IDs. Store this in a Google Sheet or as static data in the workflow using $getWorkflowStaticData('global'). Without this mapping, post to a shared channel like #sales-pipeline instead.
Step 6: Activate
- Replace
YOUR_PORTAL_IDin the Slack block with your HubSpot portal ID - Click Execute Workflow to test
- Verify Slack DMs contain the right deals and links
- Toggle to Active for daily checks
If no deals are missing fields, the Code node returns an empty array and the Slack node won't execute. No extra filter needed.
Troubleshooting
Common questions
Why does this need two separate HubSpot searches?
HubSpot's Search API applies AND logic within a filter group. You can't say "close date is missing OR amount is missing" in one query. Two searches merged in the Code node is the standard pattern.
How many n8n executions does this use per month?
30 executions (one per day). Each execution processes all deals in a single pass. On n8n Cloud Starter ($24/mo, 2,500 executions), this uses 1.2% of your quota.
Can I add more required fields beyond close date and amount?
Yes. Add another HTTP Request node for each field (e.g., next_step, hubspot_owner_id) and merge the results in the Code node. The deduplication logic handles any number of overlapping searches.
Cost
- n8n Cloud: 1 execution/day = ~30/month. Well within the Starter plan's 2,500 executions.
- Self-hosted: Free.
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.