ScheduleOnDates icon
OutputValues icon
If icon
Fail icon
Request icon
Assert icon
Get icon
Log icon
Switch icon
Loop icon
Toggle icon
Set icon
Delete icon
Pause icon
Kill icon
SlackIncomingWebhook icon
Write icon

Release freeze calendar that pauses and restores a namespace's schedules on dates

Freeze and thaw a Kestra namespace on calendar dates with ScheduleOnDates, Toggle, a KV record, an approval gate for running executions and an audit CSV.

Categories
CoreInfrastructure

Every team has dates when nothing scheduled should run: a release weekend, a peak sales period, a year-end close. On a Kestra instance that means switching off the Schedule triggers of a namespace before the window, remembering which ones, and switching exactly those back on afterwards, without touching triggers an operator had disabled on purpose. Done by hand it is a checklist that gets forgotten at the thaw end. This flow is that checklist as a calendar: named windows with a start and an end, two ScheduleOnDates triggers that fire on those dates, a KV record that links the thaw to its freeze, an approval gate for executions still running when the freeze starts, and an audit CSV per run. The sample calendar and the company.team namespace are placeholders.

How it works

  1. variables.freeze_calendar is the single source: a list of {name, start, end} with any timezone offset. freeze_start (ScheduleOnDates) reads [.[].start] from it with jq and fires once per date with inputs.phase: freeze; freeze_end reads [.[].end] and passes thaw. recoverMissedSchedules: LAST covers a scheduler that was down at that minute. Both ship disabled: true because they switch off other flows' triggers.
  2. normalize rewrites the trigger date (or the execution start) and every calendar date to UTC at minute precision. resolve finds the window: a triggered freeze matches a start, a triggered thaw an end, a manual run names it with the window input. guard_window fails a freeze or thaw without a window, because the record and the audit must say which freeze this was.
  3. inventory calls GET /api/v1/{tenant}/triggers/search for the namespace. One call returns every trigger with its definition and live state (state.disabled is what Toggle changes, state.sourceDisabled is disabled: true in the source, which no API call can clear). inventory_complete (Assert) fails above the 500 triggers one page holds: a freeze that saw only the first page would leave schedules running.
  4. scope and select are jq over that inventory. In scope: flow id matches flow_pattern, not in exclude_flows, not this controller. to_disable is in scope, a Schedule trigger, and on; triggers already off go to left_alone and are never recorded. previous_record reads the KV key release_freeze_<namespace>. plan_report prints all of it.
  5. apply (Switch on phase) is the only place that writes to other flows. freeze: refuse_double_freeze fails if a record exists (a second freeze would overwrite it), then disable (Loop, transmitFailed: false) runs one triggers.Toggle per trigger. thaw: require_record fails without a record (guessing from the current state would re-enable triggers switched off by hand), then restore toggles on exactly the recorded list.
  6. reread repeats the inventory call after the toggles. verify joins what the phase meant to change with the state after it and names each row (disabled, restored, would_disable, left_alone_*, MISMATCH_*). Nothing in the record or the audit comes from the Toggle task's own output.
  7. persist: a freeze writes the record (kv.Set, JSON, overwrite: false as a second guard, ttl: record_ttl) with window, date, execution id and the confirmed disabled list. A thaw deletes it only when every recorded trigger is back on; otherwise the next thaw retries the missing ones. This runs before the in-flight step, so a run killed during the pause still leaves a complete record.
  8. inflight searches executions of the namespace in CREATED, QUEUED, RUNNING, PAUSED or RETRYING (skipped on thaw) and keeps those whose flow is in scope. With inflight_policy: approve_kill, approve_kill pauses with a kill_inflight BOOL (default false) and a reason; executions.Kill runs only when the approver set true. A timeout resumes with the default and kills nothing.
  9. mismatch_gate fails last, listing every trigger that did not change state. finally writes the audit CSV; afterExecution posts the outcome.

How to run

Inputs: phase (preview, freeze, thaw; the triggers set it), window (calendar name for manual freeze and thaw runs), target_namespace, flow_pattern (regex over flow ids), exclude_flows (JSON array), inflight_policy (report or approve_kill), approval_timeout, record_ttl (longer than your longest window) and kestra_api_url.

  1. Add the secrets KESTRA_API_USERNAME and KESTRA_API_PASSWORD (a user that can read and update triggers in the target namespace) and, optionally, SLACK_WEBHOOK_URL.
  2. Replace the sample calendar and set target_namespace. Run with the defaults: the preview lists what would be switched off, what is already off and what is running. Nothing changes.
  3. Run phase: freeze with window set to a calendar name, then phase: thaw with the same window; check the two audit CSVs and the KV key in between.
  4. Enable freeze_start and freeze_end. The next calendar date freezes on its own, the matching end date thaws.

Resume the approval from outside the UI with POST /api/v1/{tenant}/executions/{id}/actions/resume and form fields kill_inflight, reason.

Error handling

  • checks block the execution before it exists when exclude_flows is not a JSON array or target_namespace is not a plain namespace. A blank STRING input is replaced by its default on 2.0.5, so there is no check for empty strings.
  • The three API calls have a timeout and an exponential retry. Every refusal (unknown_window, already_frozen, nothing_to_restore, incomplete) is a Fail task whose message says what to do next.
  • errors logs the failed task from errorLogs(), writes <state_key>_last_error to KV with a 30-day ttl next to the freeze record, and posts to Slack with allowFailure: true. finally writes the CSV in every case. The 12-hour sla fails a run stuck in the pause; on 2.0.5 that skips finally, so afterExecution reports it through the freeze_sla: breached label.
  • Quirks seen on 2.0.5: Toggle logs "0 triggers found to toggle" even when it worked, so the result is taken from reread; executions.Query adds one EQUALS filter per state, so several states match nothing, hence the http.Request with filters[state][IN]; flows.List is not used because its output for a large namespace exceeds the 1 MB message limit.

How to adapt

  • Several namespaces: copy the flow per namespace, or add target_namespace to the calendar entries and pass it through the trigger inputs.
  • Part of a namespace: set flow_pattern (^etl-) or list exclude_flows (alerting, on-call paging).
  • More than schedules: drop the schedule condition in select to include polling and flow triggers.
  • Chain it: start a deployment flow from afterExecution, or read the summary output from a parent flow.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.