HubSpot Deal Stage + Gmail Thread Age: Daily Stale-Deal Escalation Digest to Slack via Make

Surface every deal that's gone dark — no reply in 5+ days, still open in HubSpot — every morning before your sales standup.

The flow
HubSpot logo
Source
HubSpot
Gmail logo
Process
Gmail
Make logo
Process
Make
Slack logo
Destination
Slack

The stack in the order it runs — data flows from the source through to where it lands.

Why this stack

The single most common reason early-stage SaaS deals die isn't price or product fit — it's follow-up latency. A founder or AE moves a deal to 'Proposal Sent' in HubSpot and gets pulled into product work. Five days pass. The prospect's buying intent cools. By the time anyone checks, it's been two weeks and the deal is dead. HubSpot's built-in task reminders require someone to set them manually per deal. Nobody does.

Make (formerly Integromat) handles the Gmail API's OAuth flow and batch message retrieval cleanly through its built-in Gmail module — zero auth code written by you. The multi-router pattern lets you split logic: one route for deals in 'Proposal Sent' with no Gmail thread activity in 5+ days, another for 'Negotiation' deals with no activity in 3+ days. Zapier can't do multi-path logic in one Zap without Multi-Step premium. n8n can do it but costs you more setup time on Gmail OAuth.

The real tradeoff: Gmail thread matching to HubSpot deals only works if your team's email is logged in HubSpot via HubSpot's Gmail extension or BCC logging. If it's not, fall back to HubSpot's own `hs_last_activity_date` property — less granular, but still usable. The Slack output is one daily digest message, not one message per deal. It lists deals as bullet points with owner name, deal name, days since last contact, and a direct HubSpot deal link. That's intentional — a per-deal Slack flood gets muted inside a week.

Skip this if you're running Outreach or Salesloft — those have native sequence lapse alerts built in. This is for teams running sales entirely out of HubSpot and Gmail with no dedicated sales ops infrastructure.

The stack (4)

  1. HubSpot logo

    CRM + marketing for go-to-market.

    Pipeline data becomes an automatable input for revenue reports.

  2. Gmail logo

    Google Workspace mail with a solid API.

    Labels + the Gmail API make it easy to trigger flows off inbound mail.

  3. Make logo

    No-code automation builder. Visual scenarios that chain APIs and AI calls.

    Per-operation pricing is cheaper than Zapier at the volumes I run, and the visual editor handles branching cleanly.

  4. Slack logo

    Team chat where most ops alerts and reports land.

    The default place a small team already lives — pipe reports here instead of email nobody opens.

How it runs

  1. 1

    Create a Make scenario with a daily schedule trigger

    In Make, create a new scenario. Add a 'Schedule' trigger set to run daily at 8 AM in your local timezone. Name it 'Stale Deal Escalation – Daily'. In scenario settings, increase the timeout to 300 seconds — HubSpot's search API and Gmail batch lookups are slow when you have more than 50 open deals. Enable 'Auto-commit' so partial runs still post whatever data was collected before a timeout fires.

  2. 2

    Pull open deals from HubSpot filtered by deal stage

    Add a HubSpot 'Search CRM Objects' module. Object type: Deals. Filter: `dealstage IN ['proposal_sent', 'negotiation', 'contract_sent']` AND `closedate >= TODAY` — that drops already-closed or past-due deals. Properties to retrieve: `dealname`, `dealstage`, `hubspot_owner_id`, `hs_last_activity_date`, `hs_email_last_reply_date`, `associations.contacts`. Set result limit to 100. If you have more than 100 open deals in these stages, add a pagination iterator using Make's built-in array aggregator after the search module.

  3. 3

    Calculate days since last activity per deal

    Add a 'Set Variable' module — or use Make's Tools > Set Multiple Variables. For each deal, compute `days_since_activity = DATEDIFF(NOW(), hs_last_activity_date, 'days')` and `days_since_email_reply = DATEDIFF(NOW(), hs_email_last_reply_date, 'days')`. Use Make's `formatDate` and `dateDifference` functions — they handle timezone normalization correctly. Output both values as integers so the filter step doesn't choke on string comparisons.

  4. 4

    Filter to only genuinely stale deals

    Add a Router module with two paths. Path A: `dealstage = 'proposal_sent'` AND `days_since_activity >= 5`. Path B: `dealstage IN ['negotiation', 'contract_sent']` AND `days_since_activity >= 3`. Both paths should also filter out deals where `hubspot_owner_id` is null — unowned deals need a separate triage process, not this digest. Each path feeds into its own array aggregator to collect matching deals before building the Slack message. This is what prevents one Slack notification per deal.

  5. 5

    Look up owner name from HubSpot

    For each deal in both paths, add a HubSpot 'Get an Owner' module using `hubspot_owner_id` from the deal. Retrieve `firstName`, `lastName`, and `email`. That's what lets you show 'Owned by Sarah Chen' instead of a raw user ID in the Slack message. If you're processing a lot of deals, cache owner lookups using Make's Data Store — HubSpot's owner API isn't heavily rate-limited, but repeated identical calls on the same owner IDs slow the scenario for no reason.

  6. 6

    Build the Slack digest message from aggregated deal lists

    After each array aggregator, add a 'Text Aggregator' module that formats each deal as a single line: `• *{{dealname}}* ({{dealstage}}) — {{days_since_activity}} days idle — Owner: {{ownerName}} — <{{deal_url}}|Open in HubSpot>`. The deal URL is `https://app.hubspot.com/contacts/YOUR_PORTAL_ID/deal/{{deal_id}}`. Combine Path A and Path B text outputs using a second aggregator or a Set Variable module that concatenates both strings with a section header between them.

  7. 7

    Post the digest to Slack with conditional logic

    Add a Slack 'Create a Message' module pointing at `#sales-standup`. Only send if the total stale deal count is greater than 0 — put a Filter module before the Slack step checking that the aggregated text is not empty. Message structure: header `🔴 *Stale Deal Alert – {{TODAY}}*`, then Path A deals under `*Proposals (5+ days idle):*`, then Path B deals under `*Active Negotiations (3+ days idle):*`. Use Slack's `mrkdwn` formatting for bold and links.

  8. 8

    Post a clean confirmation when no deals are stale

    Add a second Slack message on the else path — when stale count equals 0 — posting to `#sales-standup-log`, not the main sales channel: `✅ No stale deals as of {{TODAY}}. All open deals have activity within SLA.` This creates an audit trail that the workflow ran and found nothing. That matters because if the scenario silently errors, people assume 'no alert = no stale deals' when actually nothing ran. Set this message to reply to the previous day's log message using `thread_ts` if you want to keep the channel clean.

Want me to build this for you instead?

Product Audit and CTO Mode run out of this same thinking. If you’re reading this thinking “I want this, but in my product” — let’s talk.

See services

More like this