New to Kestra?
Use blueprints to kickstart your first workflows.
Soft delete the oldest Schema Registry subject versions down to a keep_n target behind an approval gate, then re-read and fail if the subject is still over.
A Confluent Schema Registry subject gains a version every time its schema changes. The registry never removes one. A subject that has evolved weekly for two years carries more than a hundred active versions. Nothing in the registry will ever thin that list.
Nothing breaks, which is why it goes unnoticed. The cost is operational. The _schemas storage topic is compacted but never pruned, so it keeps every version of every subject forever and a new registry replica replays all of it on startup. GET /subjects/<subject>/versions turns into a wall of numbers that no one can read. And once the list is long enough, nobody can say which old versions still matter, so nobody dares touch any of them.
This blueprint prunes the history down to the newest keep_n versions per subject, oldest first, with a SOFT delete. A soft delete removes the version from the active list but leaves the global schema id resolvable through GET /schemas/ids/<id>, so a consumer that is still reading old messages whose headers carry that id keeps working. The newest version is never a target. After the prune the flow re-reads the subject. It also re-resolves every schema id it just soft deleted, so the run fails both when the subject is still over target and when a soft delete turned out to behave like a hard delete.
This blueprint was created by zkasuran.
auto_remediate true, dry_run false and a human resuming the Pause with approved=true. An unanswered resume reads as not approved.permanent_delete offers the hard delete, which does free the ids and is unrecoverable. It is off by default and needs a second field on the same approval, confirm_permanent=true, so one careless resume cannot trigger it.GET /subjects, so this flow applies the rule instead. If keep_n is 0 the run fails with the subject names rather than pruning anything.exempt_subjects are never read and never changed.keep_n is an operator judgement, not an analysis. The registry cannot tell you whether a version from last year is still referenced by messages sitting in a topic with long retention. Pick keep_n from your longest topic retention and your slowest consumer, keep the soft delete and leave permanent_delete off.list_subjects (io.kestra.plugin.core.http.Request) reads GET /subjects.list_topics (io.kestra.plugin.kafka.TopicList, read-only) returns the live topics on the cluster.scope keeps the registered subjects that back a live topic under the TopicNameStrategy (<topic>-value, <topic>-key) or the explicit subjects list, minus exempt_subjects. Internal topics are dropped.in_scope (io.kestra.plugin.core.flow.If) stops with no_subjects when nothing is in scope.audit (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) reads GET /subjects/<subject>/versions per subject and folds the active version list into a KV accumulator.assess marks a version as a target when fewer than keep_n versions sit above it. That keeps the newest keep_n whatever gaps the numbering has. With keep_n 0 it marks the subject unsafe, because the newest version would be a target.route branches on whether any subject is over target.all_within logs that there is no history to prune. No alert.log_findings logs the per subject split and alert posts it to Slack.safety_gate fails the run through refuse_latest if the target set reaches a newest version. Nothing is deleted.remediate_gate runs only when auto_remediate is true and dry_run is false. Otherwise report_only logs what would have been pruned.approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved, plus confirm_permanent when permanent_delete is set.remediate_prune (Loop, concurrencyLimit: 1) walks the targets oldest first. Per version it reads the version to capture its global schema id, soft deletes it, hard deletes it only when both permanent flags are set, then re-resolves that schema id and records the answer.verify re-reads the active versions of every pruned subject.verify_assess counts two independent failures. One is a subject still over keep_n. The other is a soft deleted version whose schema id stopped resolving.verify_result fails the run through prune_failed on either, otherwise pruned_ok logs the new counts.errors posts to Slack on any failure, for example an unreachable registry, a refused prune or a lost schema id.keep_n and walks the rest oldest first, with the newest version protected by a rule the registry does not enforce itself._schemas topic replay has grown.DELETE /subjects/<s>/versions/<v> against production.A loop of curl calls can delete versions. It cannot scope itself to the subjects that still back a live topic, refuse to touch the newest version, hold the delete for a second person or prove afterwards that the schema ids it removed from the active list are still resolvable. Kestra adds the schedule that watches the registry, the approval that turns a bulk delete into a reviewed change, the re-read that proves the result and a run history that says exactly which versions went and when.
On Kestra 2.0.5 OSS against Apache Kafka 3.9.1 (KRaft, single node) with Confluent Schema Registry 7.9.0 and a local HTTP sink standing in for Slack. Topics orders, payments and invoices were live. orders-value was set to NONE compatibility and loaded with 8 versions (global schema ids 1 to 8). payments-value held 2 versions and is the precision control. Every result was confirmed by reading GET /subjects/<s>/versions and GET /schemas/ids/<id> directly, before and after each run.
| Run | Inputs | Result |
|---|---|---|
| 1 | keep_n: 20, defaults |
2 subjects audited, 0 over target. Nothing deleted, no alert |
| 2 | keep_n: 3 |
orders-value reported at 8 active with versions 1 to 5 as targets, payments-value not flagged at 2. dry_run true so nothing deleted, GET /subjects/orders-value/versions still [1,2,3,4,5,6,7,8] |
| 3 | keep_n: 3, auto_remediate: true, dry_run: true |
Same report, no Pause, registry unchanged |
| 4 | keep_n: 0, auto_remediate: true, dry_run: false |
refuse_latest failed the run naming orders-value and payments-value, nothing deleted, errors alert sent. The refusal lands before the Pause, so an approver is never offered it |
| 5 | keep_n: 3, auto_remediate: true, dry_run: false, resumed with no answer |
Paused, resumed empty, not_approved logged, registry unchanged |
| 6 | keep_n: 3, auto_remediate: true, dry_run: false, approved=true |
Versions 1 to 5 soft deleted oldest first. Active list went [1,2,3,4,5,6,7,8] to [6,7,8], payments-value still [1,2], latest still v8. Schema ids 1 to 5 all answered 200 on GET /schemas/ids/<id> after the delete |
| 7 | keep_n: 1, approved, subject put in READONLY mode during the Pause |
Registry rejected the deletes, the re-read still showed 3 active against a target of 1, prune_failed failed the run and errors alerted |
| 8 | subjects: ["invoices-value"], keep_n: 2, permanent_delete: true, resumed with approved=true only |
not_approved. The hard delete is unreachable without confirm_permanent, invoices-value still [1,2,3,4] |
| 9 | same plus confirm_permanent=true |
Versions 1 and 2 hard deleted. Active went [1,2,3,4] to [3,4] and ?deleted=true also returned [3,4]. Schema ids 13 and 14 answered 404, ids 15 and 16 still 200. The run reported 2 ids freed and did not fail, because the hard delete was asked for |
| 10 | keep_n: 3, the same inputs as run 2 |
3 subjects audited, 0 over target. The detector went from 1 over with 5 targets to 0 over on identical inputs |
| 11 | topic invoices deleted on the broker, keep_n: 1 |
Only 2 subjects audited. invoices-value was left out of scope entirely even though it still held 2 active versions, so its history was never read or touched |
| 12 | registry URL unreachable | list_subjects failed, run failed, errors alert sent |
| 13 | every candidate in exempt_subjects |
no_subjects logged, nothing read, nothing changed |
The soft delete semantics were measured rather than assumed. After a soft delete of a version, GET /schemas/ids/<id> returns 200 with the schema and the version still appears under ?deleted=true. After a hard delete of the same version the id answers 404 Schema <id> not found, ?deleted=true does not bring it back and the version is gone from the history too. That difference is the whole reason the default here is a soft delete.
One more measurement worth stating, because it is why the newest version rule lives in this flow and not in the registry. Schema Registry 7.9.0 soft deletes the newest version of a subject on request without complaint. When that was the last active version the subject itself stopped answering GET /subjects/<s>/versions and dropped out of GET /subjects. The registry offers no protection here at all.
TopicNameStrategy. A different strategy needs the subjects input filled in explicitly.SLACK_WEBHOOK_URL: used by alert and the errors block. In Kestra OSS, provide it as the base64 encoded environment variable SECRET_SLACK_WEBHOOK_URL.| Input | Default | Purpose |
|---|---|---|
schema_registry_url |
http://localhost:8081 |
Registry base URL, no trailing slash |
bootstrap_servers |
localhost:9092 |
Brokers, read the live topic set that scopes the prune |
keep_n |
20 |
Active versions kept per subject. Must be 1 or more |
subjects |
[] |
Empty means every subject backing a live topic |
exempt_subjects |
[] |
Subjects whose history is kept whatever its length |
permanent_delete |
false |
Hard delete, which frees the schema ids and is unrecoverable |
auto_remediate |
false |
Allow the delete |
dry_run |
true |
Must be false for a delete to apply |
SLACK_WEBHOOK_URL secret.schema_registry_url plus bootstrap_servers.keep_n from your longest topic retention and your slowest consumer, then run again with dry_run true to see the exact version numbers that would go.auto_remediate: true and dry_run: false, then resume the paused execution with approved=true.weekly_scan trigger, which always runs in safe mode.keep_n per subject group by running the flow once per group with the subjects input filled in.keep_n with an age rule by reading the version metadata and keeping anything newer than a cutoff.