New to Kestra?
Use blueprints to kickstart your first workflows.
Detect a changed Redis generation key and broadcast a Pub/Sub cache invalidation message, gated by auto_remediate and dry_run.
Many systems run several app instances, each holding its own in-memory cache, with no
shared signal for when the underlying data changes. The usual fix is a generation or
version marker the data pipeline bumps on every change, paired with a fan-out broadcast
so every instance knows to invalidate at the same moment. This blueprint is that
broadcast, built as a gate rather than a bare PUBLISH: it remembers the last
generation it acted on, only considers broadcasting when that value actually changed,
and keeps a human-reviewable path between "change detected" and "every subscriber just
got told to flush its cache".
periodic_generation_check (io.kestra.plugin.core.trigger.Schedule) runs every 5 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.read_cache_generation and read_last_seen_generation (both io.kestra.plugin.redis.string.Get) read the current generation marker and the value this flow last acted on.evaluate_generation_drift (io.kestra.plugin.core.flow.If) flags a change whenever the two differ - this also covers the first run, where last_seen_key does not exist yet and GET returns null.then (changed): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers a broadcast ("true", gated again by dry_run through check_dry_run) or only logs that it is disabled ("false"). When both gates allow it, broadcast_invalidation (io.kestra.plugin.redis.pubsub.Publish) publishes the new generation value to invalidation_channel, then update_last_seen_generation (io.kestra.plugin.redis.string.Set) advances the marker so the same change is not re-broadcast next run. alert_generation_drift always fires to Slack regardless of which path ran.else (unchanged): log_generation_unchanged records a clean check.log_audit_complete always runs last, printing the headline values regardless of which branch fired.errors block alerts Slack separately if the flow itself fails - an unreachable Redis instance must not read as "nothing changed".PUBLISH leaves the next run free to retry the same generation.auto_remediate, dry_run) between "change detected" and "message actually published".redis-cli PUBLISH buried in a deploy script.pubsub.Publish paired with a change-detection gate.A raw PUBLISH has no memory of the last value it saw, no threshold or change logic, and no notification path of its own. Kestra supplies the schedule, the change detection as a first-class branch, two independent authorization gates before anything is broadcast, and an execution history that shows exactly when the generation changed and whether subscribers were told.
invalidation_channel.generation_key whenever the data it governs changes.Local testing:
docker run -d --name redis-gate -p 6381:6379 redis:7-alpine
docker network connect YOUR_KESTRA_NETWORK redis-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set redis_url to redis://redis-gate:6379/0 from inside that network, or redis://localhost:6381/0 from the host).
SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_generation_drift and the errors block.redis_url assumes an unauthenticated local instance, matching the plugin's own documentation examples. For a password-protected instance, use redis://:{{ secret('REDIS_PASSWORD') }}@<host>:<port>/0 as the redis_url value - never place the password directly in the input.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_SLACK_WEBHOOK_URL=$(echo -n 'https://hooks.slack.com/...' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.redis_url (STRING, default redis://localhost:6379/0): connection string shared by every Redis task.generation_key (STRING, default cache:generation): marker bumped by the data pipeline on every change.last_seen_key (STRING, default cache:last_seen_generation): marker this flow uses to avoid re-broadcasting the same change.invalidation_channel (STRING, default cache:invalidate): Pub/Sub channel subscribers listen on.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for a broadcast to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for PUBLISH to actually run.outputs.read_cache_generation.data / outputs.read_last_seen_generation.data: the two compared values for every run.outputs.broadcast_invalidation.count: number of values published (not subscriber count), present only when a broadcast actually ran.redis-cli -p 6381 SET cache:generation v1.SLACK_WEBHOOK_URL secret.evaluate_generation_drift detects a change on this first run (no last_seen_key exists yet) and, with auto_remediate: "false", log_remediation_disabled logs without publishing.auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_invalidation describes the broadcast without sending it.auto_remediate: "true" and dry_run: "false", then bump the generation (redis-cli -p 6381 SET cache:generation v2) and run again to confirm a fresh broadcast fires only on the real change, not on an unchanged generation.periodic_generation_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.invalidation_channel (any Redis client's SUBSCRIBE) to see the full loop end to end.io.kestra.plugin.redis.json.Get instead of string.Get if the generation marker is stored as a JSON document rather than a plain string.generation_key/invalidation_channel pairs with io.kestra.plugin.core.flow.Loop to watch more than one cache domain from a single flow.serdeType: JSON, from a map with the old and new generation) instead of a bare string, if subscribers need more context than the new value alone.generation_key that was never set is indistinguishable from "unchanged". failedOnMissing defaults to false, so a missing key returns data: null rather than failing; if both generation_key and last_seen_key are missing, null != null is false and no drift is reported. Verify the data pipeline is actually writing generation_key before trusting a quiet run.count on broadcast_invalidation is not a subscriber count. Verified from source: the task sums each PUBLISH call's own subscriber-count return value internally, then discards it and returns 1 per published value regardless. count only tells you how many values were sent, never how many subscribers received them.last_seen_key only advances after the broadcast task runs. If broadcast_invalidation fails, update_last_seen_generation never executes, so the next run retries the same change instead of silently skipping it.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for PUBLISH to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.