Trigger icon
Fail icon
GetKeys icon
Download icon
Eval icon
If icon
Loop icon
Sequential icon
Dag icon
Script icon
Docker icon
Delete icon
Set icon
Concat icon
DeduplicateItems icon
IonToCsv icon
FilterItems icon
IonToJson icon
Split icon
Log icon
SlackIncomingWebhook icon
Get icon

IANA Time Zone Database Release Impact Watcher

Detect new IANA tzdata releases with an HTTP trigger, diff zone rules, match your time zone inventory and alert owning teams before DST or offset changes.

Categories
CoreInfrastructure

Governments change clocks at short notice: in 2026 British Columbia, Alberta, the Northwest Territories and Manitoba dropped DST and Morocco moved to permanent +00. Schedulers, billing cut-offs and calendars break when hosts keep old time zone data. This flow notices a new IANA tz database release soon after it is published, finds which of your zones changed, and tells each owning team the new offset, the date and the days left. The default inventory is SAMPLE DATA (made-up teams and systems). The version file, NEWS and release tarballs come live from IANA.

How it works

  1. new_tzdb_release (HTTP trigger) polls the 6-byte version file every 30 minutes and starts a run only when it differs from tzwatch_last_version in KV.
  2. fetch_news downloads NEWS. plan (GraalVM JS) reads its release headers and picks the work: every release since the stored version (at most max_releases), only the latest on the first run (BASELINE), replay_version, or the dead-letter list (mode: replay_failed). If the version file is ahead of NEWS, it waits for the next poll.
  3. analyze_releases (Loop, two at a time) runs one try/catch per release. Inside it, the fetch_both Dag downloads tzdataPREV.tar.gz and tzdataVER.tar.gz in parallel. Then diff_release (Python, standard library) reads both tarballs in memory, builds each zone's offset and abbreviation timeline, and compares old and new up to horizon_days ahead.
  4. Each changed name gets a class: FUTURE, IN_EFFECT (already differs today), PAST (history only), NO_EFFECT (source edits only), RENAMED (now a link to a new name, like Europe/Kiev to Europe/Kyiv in 2022b), MERGED (folded into another zone, pre-1970 data moved to backzone), BACKZONE, REMOVED or NEW_NAME. Links such as Canada/Central resolve to their target. The release's NEWS section is extracted and checked against the diff.
  5. The inventory is matched. A bad row (unknown zone, missing team) becomes an INVALID record; the batch carries on.
  6. merge_impacts joins the release files. dedupe_impacts keeps the newest record per team and zone, alertable_impacts keeps FUTURE, IN_EFFECT, RENAMED and REMOVED, unsent_impacts drops records already in the alert ledger, and split_by_team writes one file per team. notify_teams logs a WARN and posts to Slack for each team, then writes one ledger key per record.
  7. advance_pointer stores the latest version, in watch mode only.

What this blueprint teaches

HTTP trigger instead of Schedule plus If

A Schedule starts an execution on every tick and an If inside finds nothing to do, so a quiet month leaves thousands of empty runs. io.kestra.plugin.core.http.Trigger evaluates responseCondition in the scheduler and creates an execution only when the version differs from KV, never while its last run is still going. The pointer moves only after a successful run, so the next poll retries a failed one. It polls ftp.iana.org because the CDN copy on data.iana.org can be a day old.

Storage tasks on record files, no database

DeduplicateItems keeps the last record per expr, so order matters: merge_impacts concatenates the oldest release first. FilterItems filters on typed fields ({{ alert }}) and can call kv() per item, which is how unsent_impacts makes re-runs idempotent. Split with regexPattern groups lines by team. JSON lines are valid Ion, so no conversion is needed before the filters.

Dag inside a Loop

fetch_both declares dependsOn instead of nesting Parallel and Sequential: both downloads start together and the diff waits for both.

Per-release try/catch, dead letters and replay

release_try has allowFailure and its own errors branch. A failed release becomes a WARNING iteration and record_dead_letter stores tzwatch_dlq_VERSION (reason, mirror, attempts) while the others carry on. require_one_release fails the run when nothing succeeded; flag_partial_result marks a partial run WARNING. mode: replay_failed retries the list and clear_dead_letter heals it.

errors, finally, afterExecution, sla, checks and KV TTL

errors logs and posts the failed task and execution link; failure_mute, a KV key with a 6 hour ttl, limits an outage to one alert while the trigger retries. finally writes the run summary. afterExecution covers WARNING, KILLED and SLA breaches. run_deadline fails a run after 30 minutes and labels it. checks reject a malformed inventory or conflicting replay inputs before the execution exists. Ledger keys expire 30 days after their change takes effect.

What you get

  • One message per team with zone, systems, old and new offset, local time and days left, plus a CSV of every matched record (impact_report).
  • No duplicate pages: re-runs, overlapping catch-ups and replays skip ledgered records.
  • Releases that only change past timestamps, code or docs are logged, not alerted.
  • Backoff retries and timeouts on every download, dead letters with reasons, one outage alert per 6 hours.

Who it's for

  • Platform and SRE teams who own schedulers, cron fleets and base images.
  • Billing, payroll and calendar teams whose cut-offs depend on local time.

Why orchestrate this with Kestra

  • The trigger, state, retries, alert mute and ledger are declared, not coded.
  • Each release has its own logs and failure record, and replay is one input.

Prerequisites

  • Kestra 2.0 with the Python (Docker runner, python:3.13-slim), GraalVM, serdes and Slack plugins.
  • Outbound HTTPS to ftp.iana.org and data.iana.org, or to your own mirror. No API key.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook for team and failure alerts (optional; WARN logs are the alert of record).

Quick start

  1. Replace inventory with your zones, owning teams and systems.
  2. Execute once. With no stored version the run is a BASELINE that analyses only the latest release.
  3. To see a catch-up, set KV tzwatch_last_version to an older release such as 2026b; the next poll processes every release since.
  4. Try replay_version: 2022b for RENAMED and MERGED records, or 2025c for a past-only release.
  5. Add SLACK_WEBHOOK_URL.

How to extend

  • Route each team to its own channel by adding a secret name to the inventory.
  • Load the inventory from a CMDB or from TZ settings in your repositories.
  • Open a ticket per FUTURE record and close it when hosts report the new tzdata.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.