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

medium complexityCost: $0-24/moRecommended

Prerequisites

Prerequisites
  • n8n instance (cloud or self-hosted)
  • HubSpot private app token with crm.objects.deals.read scope
  • Slack app with Bot Token (chat:write scope)
  • n8n credentials for HubSpot and Slack
  • A mapping of HubSpot owner IDs to Slack user IDs (Google Sheet or static data)

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
}
Why two searches?

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."
        }
      ]
    }
  ]
}
Owner to Slack user mapping

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

  1. Replace YOUR_PORTAL_ID in the Slack block with your HubSpot portal ID
  2. Click Execute Workflow to test
  3. Verify Slack DMs contain the right deals and links
  4. Toggle to Active for daily checks
Skipping empty results

If no deals are missing fields, the Code node returns an empty array and the Slack node won't execute. No extra filter needed.

Cost

  • n8n Cloud: 1 execution/day = ~30/month. Well within the Starter plan's 2,500 executions.
  • Self-hosted: Free.

Need help implementing this?

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