OutputValues icon
Query icon
Count icon
Log icon
If icon
SlackIncomingWebhook icon
Fail icon
Pause icon
Loop icon
Request icon
Resume icon
Sleep icon
Schedule icon

Recover Kestra executions abandoned in PAUSED

Find Kestra executions stuck PAUSED past a grace window, alert Slack, then behind an allowlist and a Pause approval answer each one and verify it resumed.

Categories
CoreInfrastructure

An execution that hit a Pause waiting for a human answer, then never got one, sits PAUSED for ever. It holds its concurrency budget. Its downstream never runs. Because the execution is not FAILED no failure alert fires. Weeks later nobody remembers what it was waiting for. This flow finds those executions, reports them, then answers them under conditions you set.

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 guards itself.

This blueprint was created by zkasuran.

Safety

Resuming a pause means answering an approval nobody answered. A blind resume can approve a destructive action that a human deliberately walked away from. This flow is for pauses abandoned by accident, never for approving work on somebody's behalf. The allowlist is what keeps those two apart.

  • allow_flows is empty by default, so out of the box nothing is ever resumed. You name the flows whose pauses you have decided are safe to answer, one flow id at a time. A paused execution of any other flow is reported in the logs, in Slack, then left alone.
  • min_paused_minutes is a grace window. A pause younger than the window is somebody's live decision, not an abandoned one, so it is never a candidate.
  • required_pause_task pins the task id the candidate must be waiting on. A candidate paused somewhere else is skipped, the re-verify then sees it still PAUSED, so the run fails rather than passing quietly.
  • resume_answer is the answer you wrote, sent verbatim as the pause's onResume payload. The flow never invents an approval value.
  • max_resumes refuses the batch rather than answering it when the pile is large. A large pile means approvals are not being answered at all, which is a process problem.
  • A resume runs only when all of these hold: auto_remediate true, dry_run false, the flow on allow_flows, the count at or under max_resumes, a human approving this flow's own Pause. Any missing step changes nothing.
  • After the resumes it queries the target again and fails the run if any targeted execution is still PAUSED.

How it works

  1. window freezes the grace-window cutoff once, so every later comparison uses the same instant instead of drifting between tasks.
  2. detect (io.kestra.plugin.kestra.executions.Query, read-only) fetches every PAUSED execution in the namespace with fetchType: FETCH. Its rows carry state.histories, which is where the moment an execution went PAUSED comes from. Execution start date is the wrong field here: a long run that paused a minute ago started hours ago.
  3. paused_total (io.kestra.plugin.kestra.executions.Count, read-only) asks the same cluster for its own PAUSED count, so the detail query is checked against the target rather than trusted.
  4. select and pick map each row to its PAUSED timestamp, drop this flow plus this execution, keep what is past the cutoff, then narrow that to allow_flows.
  5. counts emits the ids, the stale detail, the allowlisted detail plus what the allowlist held back. summary logs all of it, which is the whole output in dry run.
  6. route (io.kestra.plugin.core.flow.If):
    • nothing stale: clean logs how many pauses are inside the window.
    • stale found: alert posts to Slack.
      • report_only logs what would be answered unless auto_remediate is true with dry_run false.
      • eligible_gate stops at none_eligible when the allowlist leaves nothing.
      • ceiling fails the run rather than answering when more than max_resumes are eligible.
      • approval (io.kestra.plugin.core.flow.Pause) waits for a human. No answer changes nothing.
      • recover (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) reads each execution with detail, checks required_pause_task in task_gate, then answers it with io.kestra.plugin.kestra.executions.Resume carrying your resume_answer.
      • settle, then reverify queries PAUSED again and verify_counts intersects that with the ids targeted.
      • verify_gate fails the run when any targeted execution is still PAUSED. It logs recovered when none is.

What you get

  • A named list of the executions nobody answered, with the flow id plus how long each has been waiting, in the logs and in Slack.
  • A recovery that is bounded four ways: a grace window, a flow allowlist, an expected task id, a per-run ceiling.
  • An answer you control. The onResume payload is your JSON, not a hardcoded approval.
  • A re-query that fails the run when a resume did not land, so a silent no-op cannot pass as a fix.
  • A disabled hourly schedule that runs in report-only mode when you enable it.

Who it's for

Platform teams running Kestra with human-in-the-loop approvals. If any flow of yours pauses for a release sign-off, a data-quality sign-off or a cost sign-off, some of those pauses are already stale and nothing is telling you. Also for anyone who has watched a scheduled flow queue behind a pause from three weeks ago.

Why orchestrate this with Kestra

The detection, the approval gate plus the write all live in one versioned flow, against Kestra's own typed plugin rather than curl. The resume is itself gated by a Pause, so answering somebody else's approval needs an approval, which is the shape this problem deserves. Kestra keeps the full run history, so every recovery has a record of who approved it, what answer was sent, which executions moved. The schedule means you find the stale pauses rather than remembering to look.

Tested end to end

Run against Kestra 2.0.5 on a dedicated instance, never a shared one, with two flows pausing on named approval tasks.

  • Grace window, nothing to do: two pauses 30 seconds old with a 60 minute window, zero stale, clean logged.
  • Detection with identical inputs once one pause aged past a 1 minute window: one stale, named by flow id plus id.
  • dry_run=true: the stale pause named in the log plus Slack, both executions still PAUSED afterwards, confirmed by id through the target's own API.
  • auto_remediate=true, dry_run=false with an approved Pause: the allowlisted execution went PAUSED to SUCCESS, its onResume answer arrived as sent, the re-query found it gone.
  • Declined Pause: nothing resumed, target still PAUSED.
  • Allowlist exclusion: the non-allowlisted execution stayed PAUSED through every remediating run, checked by id each time.
  • Per-run ceiling: two eligible with max_resumes: 1 failed the run before the approval.
  • Re-verify failure: required_pause_task set to a task the candidate is not on left it PAUSED, the re-query caught it, the run FAILED.
  • Failure path: an unreachable kestra_url fired the errors: Slack block.

Prerequisites

Secrets

  • SLACK_WEBHOOK_URL: incoming webhook for the alert plus the failure notice.
  • KESTRA_PASSWORD: password for the API user on the target cluster. Default of the kestra_password input, so it is never written into the flow.

The API user needs read on executions in the namespace plus permission to resume them.

Inputs

  • kestra_url: target Kestra API base URL. Defaults to the local cluster.
  • namespace: the only namespace considered.
  • min_paused_minutes: grace window before a pause counts as abandoned. Default 60.
  • allow_flows: flow ids whose pauses may be answered. Empty by default, which resumes nothing.
  • required_pause_task: expected paused task id. Empty skips the task check.
  • resume_answer: JSON object sent as the onResume answer. Default {"approved": true}.
  • max_resumes: ceiling per run. Default 5.
  • kestra_user, kestra_password: API credentials.
  • auto_remediate, dry_run: the write gate. Defaults are report-only.

Quick start

  1. Add SLACK_WEBHOOK_URL plus KESTRA_PASSWORD to your namespace secrets.
  2. Run it as shipped with dry_run=true. You get the list of abandoned pauses, nothing changes.
  3. Read the list. For each flow on it, decide whether its pause was abandoned by accident or walked away from on purpose. Only the accidental ones belong in allow_flows.
  4. Put those flow ids in allow_flows, set required_pause_task to the approval task id, set resume_answer to the answer that flow's onResume expects.
  5. Run with auto_remediate=true plus dry_run=false, then answer the approval pause with approved: true. Check the recovered log line.
  6. Enable the hourly trigger once you are happy. It runs report-only, so it alerts without touching anything.

How to extend

  • Swap the Slack task for io.kestra.plugin.notifications.pagerduty or Opsgenie when a pause older than a day is an on-call matter.
  • Add a second window: alert at 60 minutes, only allow a resume past 24 hours, so a human has a day to answer before anything automated does.
  • Kill rather than answer. Point the loop at io.kestra.plugin.kestra.executions.Kill for pauses whose approval window has closed, where cancelling is safer than approving.
  • Record every recovery in a table with io.kestra.plugin.jdbc.postgresql.Query so the audit trail outlives the execution retention.
  • Widen the scope with io.kestra.plugin.kestra.namespaces.NamespacesWithFlows plus a loop over namespaces, keeping one allowlist per namespace.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.