New to Kestra?
Use blueprints to kickstart your first workflows.
Find Kestra executions stuck in RUNNING past a threshold, alert Slack, then behind a Pause approval kill each one and verify it is gone.
When a worker dies mid task, its execution can sit in RUNNING for ever. Nothing fails and no alert fires. The execution keeps holding a concurrency slot, so the next scheduled run queues behind a job that will never finish. A failure alert cannot catch this, because the execution never failed. This flow finds those stuck executions. It is the piece most monitors leave out: it does the kill, behind an approval, then proves the kill landed.
It talks to the Kestra API of whichever instance you point kestra_url at. The default is the
cluster the flow runs on, so it reaps itself.
This blueprint was created by zkasuran.
Killing executions is destructive, so the scope is narrow by default and the kill is gated.
namespace. The flow_id input can narrow it to a single flow.exclude_flows lists flows that are allowed to run long.auto_remediate is true, dry_run is false, the count
is at or under max_kills, a human approves the Pause. Any missing step keeps everything.detect (io.kestra.plugin.core.http.Request, read-only) calls GET /api/v1/main/executions/search
with filters[state][EQUALS]=RUNNING, filters[namespace][EQUALS] and
filters[startDate][LESS_THAN_OR_EQUAL_TO] set to now minus the threshold. The modern
filters[...] form is used on purpose: the flat state=/namespace= form is ignored on 2.0.5
and returns everything, which is the wrong answer for a kill.select and counts drop this reaper's own flow and execution, apply the flow_id filter and
exclude_flows, then emit the ids and a one-line detail of what is stuck.summary logs how many are stuck and how many were selected.route (io.kestra.plugin.core.flow.If):clean logs it.alert posts to Slack.remediate_gate runs only when auto_remediate is true and dry_run is false.ceiling fails the run rather than killing when more than max_kills are stuck.approval (io.kestra.plugin.core.flow.Pause) waits for a human. No answer keeps everything.reap (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) kills each id with
io.kestra.plugin.kestra.executions.Kill.reverify reads the API again and verify_gate fails if any targeted id is still RUNNING.errors posts to Slack when the API cannot be reached.hourly (io.kestra.plugin.core.trigger.Schedule), shipped disabled and in safe mode.The reaper is itself a Kestra flow, so it needs no extra infrastructure to watch Kestra. It runs on
a schedule, reports to Slack, holds the kill behind an approval, kills one execution at a time, then
keeps a full execution history of every decision. Point kestra_url at any instance to reap a
remote cluster from one place.
On Kestra 2.0.5 OSS. To keep real executions safe the reaper pointed at a second Kestra 2.0.5 as the target, with a local HTTP sink standing in for Slack. The target held one throwaway flow whose only task sleeps for 30 minutes, so every run of it sits in RUNNING. I seeded several and drove each path. Before and after counts come from the target's own executions API, not only the flow log.
| Run | Inputs | Result |
|---|---|---|
| Clean | threshold 120 min | 0 stuck, clean branch, run SUCCESS |
| Detect and report | threshold 0, dry_run: true |
4 stuck found and listed, Slack alert sent, nothing killed, the 4 still RUNNING afterwards |
| Remediate | threshold 0, auto_remediate: true, dry_run: false |
4 found, Pause approved, 4 killed, the re-query found 0 still RUNNING, the target API confirmed all 4 KILLED. RUNNING went from 4 to 0 |
| Ceiling | threshold 0, max_kills: 2 with 3 stuck |
run failed before any kill, the 3 left RUNNING |
| Re-verify gate | kill step disabled in a copy | the 3 stayed RUNNING, the re-query caught them, the run failed instead of reporting success |
| Target down | wrong port | detect failed, the errors block posted to Slack |
kestra_url reachable from the Kestra worker. The default http://localhost:8080 works when the
flow reaps the cluster it runs on.KESTRA_PASSWORD: the API password, used as the default for the kestra_password input.SLACK_WEBHOOK_URL: used by alert and the errors block.SECRET_.| Input | Default | Purpose |
|---|---|---|
kestra_url |
http://localhost:8080 |
Target Kestra API, the cluster to reap |
namespace |
company.team |
The one namespace considered |
flow_id |
empty | Narrow to a single flow, empty means every flow in the namespace |
max_running_minutes |
30 |
A run older than this while RUNNING is stuck |
exclude_flows |
[] |
Flows allowed to run long |
max_kills |
25 |
Refuse to kill more than this in one run |
kestra_user |
admin@kestra.io |
API username |
kestra_password |
{{ secret('KESTRA_PASSWORD') }} |
API password |
auto_remediate |
false |
Allow the kill |
dry_run |
true |
Must be false for a kill to run |
KESTRA_PASSWORD and SLACK_WEBHOOK_URL secrets.namespace, then point kestra_user at a user that can kill executions.auto_remediate: true and dry_run: false, then approve the
Pause.hourly trigger, which always runs in safe mode.kestra_url values to reap more than one cluster.