New to Kestra?
Use blueprints to kickstart your first workflows.
Search HubSpot for open deals stalled past a threshold, alert Slack with the count, and log the result for RevOps pipeline hygiene.
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.
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.evaluate (io.kestra.plugin.core.output.OutputValues) compares the matched count
(total) against max_stalled_count.route (io.kestra.plugin.core.flow.If):summary logs the count and severity, closing the governance cycle.errors alerts Slack if the flow itself fails, for example an expired API key.outputs.search_stalled_deals.uri)
for whoever follows up, not just the headline count.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.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.
crm.objects.deals.read).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.
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.| 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.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.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.apiKey
property; only one of apiKey or oauthToken should be set.HUBSPOT_API_KEY and SLACK_WEBHOOK_URL secrets.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).summary logs a count.stalled_days to something you know will match to confirm alert fires.daily trigger once the thresholds reflect your actual sales cycle.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.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.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.