A schedule gets switched off for a good reason. It pages too much during an incident. Or a new
guard ships off while someone watches it. Then nobody turns it back on. The schedule never fires
again and nothing complains, because the thing that would have complained is the thing that is
off. No error, no alert, no execution. Every dashboard looks calm because nothing is running
to report. The failure is the absence of work, which is the hardest kind to notice.
This flow reads the trigger state of whichever Kestra you point kestra_url at, finds the
triggers that are off, then switches back on only the ones an operator has written down as
desired_live. Then it reads the state again and fails the run if a trigger it targeted is
still off.
This blueprint was created by zkasuran.
Safety
Restarting a schedule somebody stopped on purpose is worse than leaving it off, so the allowlist
is the whole safety model, not a convenience.
- It acts ONLY on triggers listed in
desired_live as flowId/triggerId. A trigger that is off
and not on the list is reported in the log plus the Slack message, then left exactly as it is.
With the default empty list the flow changes nothing at all.
- It looks at one
namespace.
- A change runs only when all of these hold:
auto_remediate is true, dry_run is false, the
count is at or under max_enables, a human approves the Pause. Any missing step changes
nothing.
- After the change it reads the trigger state again and fails the run if any targeted trigger is
still off.
Findings
Two things turned up while running this against a real Kestra 2.0.5. Both shape the flow.
- A trigger can be off in two different ways and only one of them is fixable from the API.
GET /api/v1/main/triggers/search returns state.disabled, the stored flag an operator flips
in the UI, next to state.sourceDisabled, which mirrors disabled: true in the flow source.
io.kestra.plugin.kestra.triggers.Toggle writes the stored flag only. A source-disabled
trigger stays off whatever the API does, because the fix is a change to the flow file. So the
flow sorts the two apart, switches on the stored-flag ones, then reports the source-disabled
ones as needing a code change instead of pretending to fix them.
Toggle logs 0 triggers found to toggle on a write that landed. It reads a count field the
2.0.5 API does not send, so the number is always zero. The re-read is the only trustworthy
signal, which is exactly why this flow fails on the re-read rather than on the task log.
How it works
detect (io.kestra.plugin.core.http.Request, read-only) calls
GET /api/v1/main/triggers/search with filters[namespace][EQUALS] set to the namespace.
The modern filters[...] form is used on purpose, because the flat namespace= form is
ignored on 2.0.5 and returns every trigger on the instance.
select sorts the switched-off triggers into three sets: targets (on the allowlist, stored
flag off, fixable here), blocked (on the allowlist, off in the flow source) plus ignored
(off and not on the allowlist).
counts and summary report all three sets with the last time each trigger fired.
route (io.kestra.plugin.core.flow.If):
- nothing off:
clean logs it.
- something off:
alert posts all three sets to Slack.
remediate_gate runs only when auto_remediate is true and dry_run is false.
act_gate skips straight to nothing_fixable when every off trigger is either source
disabled or not allowlisted.
ceiling fails the run rather than acting when more than max_enables are off.
approval (io.kestra.plugin.core.flow.Pause) waits for a human. No answer changes nothing.
reenable (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) switches each one on
with io.kestra.plugin.kestra.triggers.Toggle and enabled: true.
reverify reads the trigger state again. verify_gate fails the run if any targeted
trigger is still off. If they are all live it logs the next scheduled run of each one.
errors posts to Slack when the target cannot be reached or the verify fails.
daily (io.kestra.plugin.core.trigger.Schedule), shipped disabled and in safe mode.
What you get
- The triggers in a namespace that are switched off, each with the last time it fired, so a
schedule that has not run in weeks is obvious.
- The ones you declared should be live, switched back on, scoped by an allowlist, approved by a
human and capped.
- The ones that are off in the flow source, named separately, because those need a pull request
rather than an API call.
- A second read of the trigger state as proof, with the next scheduled run of each trigger, so
the result is work resuming rather than a flag that moved.
Who it's for
- Platform teams who disable a noisy schedule during an incident and need something that
remembers to turn it back on.
- Anyone who has found a nightly job that quietly stopped running months ago.
- Teams who want the remediation step a trigger monitor stops short of, without it restarting
something that was paused deliberately.
Why orchestrate this with Kestra
The guard is itself a Kestra flow, so watching Kestra needs no extra infrastructure. It runs on a
schedule, posts to Slack, holds the change behind an approval, acts on one trigger at a time,
then keeps the whole decision in the execution history. Point kestra_url at any instance to
check a remote cluster from one place.
Tested end to end
On Kestra 2.0.5 OSS. The flow ran on one Kestra and pointed kestra_url at a second throwaway
Kestra, so no real trigger was ever touched. A local HTTP sink stood in for Slack. The target
held five flows with Schedule triggers in one namespace: two switched off through the API and on
the allowlist, one switched off and deliberately NOT on the allowlist, one left enabled and on
the allowlist, one carrying disabled: true in its own source. Before and after values come from
the target's own triggers/search API, not from the flow log.
| Run |
Inputs |
Result |
| Clean |
allowlist of one enabled trigger |
0 off, clean branch, run SUCCESS |
| Detect and report |
3 off, dry_run: true |
2 targets, 1 source blocked, 1 not allowlisted, Slack alert sent, all 3 still off afterwards |
| Remediate |
auto_remediate: true, dry_run: false |
Pause approved, 2 switched on, re-read found 0 still off, the target API confirmed both stored flags cleared. The non-allowlisted trigger stayed off |
| Work resumed |
same run, one target on a one minute cron |
the re-enabled trigger fired a real execution within the minute, proven by a new execution on the target plus a moving lastTriggeredDate |
| Nothing fixable |
allowlist of only the source-disabled trigger |
reported as needing a flow change, no Toggle issued, run SUCCESS |
| Declined |
Pause resumed without approval |
nothing switched on, both targets still off |
| Ceiling |
max_enables: 1 with 2 off |
run failed before any change, both left off |
| Re-verify gate |
a copy that also targets the source-disabled trigger |
Toggle could not clear it, the re-read caught it, the run failed instead of reporting success |
| Target down |
wrong port |
detect failed, the errors block posted to Slack |
Prerequisites
- A Kestra API user that can read and update triggers in the namespace.
kestra_url reachable from the Kestra worker. The default http://localhost:8080 works when
the flow checks the cluster it runs on.
Secrets
KESTRA_PASSWORD: the API password, used as the default for the kestra_password input.
SLACK_WEBHOOK_URL: used by alert and the errors block.
- In Kestra OSS, provide each one as a base64-encoded environment variable prefixed
SECRET_.
Inputs
| Input |
Default |
Purpose |
kestra_url |
http://localhost:8080 |
Target Kestra API |
namespace |
company.team |
The one namespace considered |
desired_live |
[] |
Allowlist of flowId/triggerId that must be live |
max_enables |
10 |
Refuse to switch on 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 change |
dry_run |
true |
Must be false for anything to change |
Quick start
- Add the
KESTRA_PASSWORD and SLACK_WEBHOOK_URL secrets.
- Run it once with the defaults to list every trigger in the namespace that is off.
- Take the ones that should be live from that list and put them in
desired_live as
flowId/triggerId.
- Run with
auto_remediate: true and dry_run: false, then approve the Pause.
- Enable the
daily trigger, which always runs in safe mode.
How to extend
- Open a ticket per source-disabled trigger, since those need a change to the flow file.
- Keep
desired_live in Git beside the flows, so the list of schedules that must be live is
reviewed like code.
- Alert on how long a trigger has been off, read from
lastTriggeredDate, so a schedule off for
a week pages louder than one off for an hour.
- Loop over several
kestra_url values to check more than one cluster.
Links