Search icon
OutputValues icon
If icon
SlackIncomingWebhook icon
Log icon
Schedule icon

HubSpot Stalled Deal Pipeline Sentinel

Search HubSpot for open deals stalled past a threshold, alert Slack with the count, and log the result for RevOps pipeline hygiene.

Categories
BusinessCore

A deal that stops getting touched rarely announces itself. HubSpot's own stale-deal view is a saved filter someone has to remember to open, and its native workflows can notify the deal owner but have no simple way to roll that up into one governance signal for the team. This blueprint runs that same question through HubSpot's CRM Search API on a schedule and turns it into a Slack page with a count attached, not a dashboard nobody checks.

This blueprint was created by Mohit Jeswani.

How it works

  1. search_stalled_deals (io.kestra.plugin.hubspot.deals.Search) runs a HubSpot CRM search with a server-side filter group: hs_lastmodifieddate older than stalled_days, dealstage not in excluded_stages, and amount at or above min_deal_amount. Read-only; nothing in HubSpot is changed.
  2. evaluate (io.kestra.plugin.core.output.OutputValues) compares the matched count (total) against max_stalled_count.
  3. route (io.kestra.plugin.core.flow.If):
    • compliant: logs that nothing matches.
    • alert: posts the count to Slack along with the internal storage URI of the full matched-deal list.
  4. summary logs the count and severity, closing the governance cycle.
  5. errors alerts Slack if the flow itself fails, for example an expired API key.

What you get

  • A single number, not a dashboard: how many open deals above your size threshold have gone quiet, every run.
  • A filter that already excludes closed deals, so a backlog of old, correctly-closed deals never shows up as a false positive.
  • The full matched list retained in Kestra's internal storage (outputs.search_stalled_deals.uri) for whoever follows up, not just the headline count.

Who it's for

  • RevOps and sales leadership who want one governance signal for pipeline hygiene instead of relying on reps to notice their own stalled deals.
  • Teams evaluating the HubSpot plugin who want a worked example of deals.Search with filterGroups, distinct from this repo's existing hubspot-query-company.yml and create-hubspot-ticket-on-failure.yaml, neither of which search or filter deals.

Why orchestrate this with Kestra

HubSpot workflows can notify a deal's owner when it goes stale, but there is no native rollup that answers "how many, total, across the whole pipeline" on a schedule with its own alert and history. Kestra keeps stalled_days, min_deal_amount and the excluded stages as flow inputs, turns the search into a single Slack number, and keeps a full execution history of every check.

Prerequisites

  • A HubSpot account with a private app (or legacy API key) that has CRM read scope on deals (crm.objects.deals.read).
  • At least one open deal to confirm the filter logic against.
  • A Slack incoming webhook URL.

This flow calls the HubSpot API directly over HTTPS; there is no official local HubSpot emulator, so search_stalled_deals cannot be exercised against a local Docker container. Use a HubSpot developer test account to validate this end to end.

Secrets

  • HUBSPOT_API_KEY: a HubSpot private app token (or legacy API key) with deals read scope, sent as Authorization: Bearer. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_HUBSPOT_API_KEY.
  • SLACK_WEBHOOK_URL: used by alert and the errors block. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_SLACK_WEBHOOK_URL.

Inputs

Input Default Purpose
stalled_days 14 Days since hs_lastmodifieddate that counts as stalled
min_deal_amount 0 Only deals at or above this amount are considered
excluded_stages [closedwon, closedlost] Internal dealstage IDs never reported stalled
max_results 100 HubSpot search page size (1-100)
max_stalled_count 1 Matched-deal count that triggers the Slack alert

Outputs

  • {{ outputs.search_stalled_deals.total }}: number of matching deals.
  • {{ outputs.search_stalled_deals.uri }}: internal storage URI of the full matched-deal list (dealname, dealstage, amount, hs_lastmodifieddate, closedate per deal).
  • {{ outputs.evaluate.values.severity }}: OK or STALLED_DEALS_FOUND.

Pitfalls

  • excluded_stages must match your pipeline's internal stage IDs, not their display labels - a custom pipeline with renamed or additional closed stages that aren't listed here will have those deals reported as stalled forever.
  • max_results caps at HubSpot's own limit of 100 per search call; this task does not paginate beyond limit (it uses Search, not fetchAllPages behavior here), so a pipeline with more than 100 matching stalled deals only surfaces the first page, sorted oldest-stalled-first so the most overdue ones are never the ones cut off.
  • hs_lastmodifieddate updates on any property change, not just stage movement - a deal where someone edited an unrelated field yesterday will not show up as stalled even if its stage hasn't advanced in months.
  • A HubSpot legacy API key and a private app token are both accepted via the same apiKey property; only one of apiKey or oauthToken should be set.

Quick start

  1. Add the HUBSPOT_API_KEY and SLACK_WEBHOOK_URL secrets.
  2. Confirm excluded_stages matches your pipeline's actual closed-stage IDs (check a closed deal's dealstage value via the HubSpot API or a Get call).
  3. Run the flow with the defaults and confirm summary logs a count.
  4. Lower stalled_days to something you know will match to confirm alert fires.
  5. Enable the daily trigger once the thresholds reflect your actual sales cycle.

How to extend

  • Loop per sales owner with io.kestra.plugin.core.flow.Loop and a filterGroups entry on hubspot_owner_id to send each rep their own stalled-deal count instead of one team-wide total.
  • Add a second, lower min_deal_amount threshold routed to a different, lower-priority Slack channel, so large stalled deals and small ones do not compete for the same alert's attention.
  • Read outputs.search_stalled_deals.uri in a downstream task to post the actual deal names, not just the count, once you've confirmed the volume is manageable for a single message.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.