Find icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Delete icon
Fail icon
Schedule icon

MongoDB Expired Document Retention Guard

Detect MongoDB documents past a retention window, alert Slack, then delete them behind an approval and re-check that none remain.

Categories
DataInfrastructure

MongoDB has one built-in way to drop old documents: a TTL index on a date field. When that index was never created or someone dropped it, nothing deletes the old documents. The collection grows without bound and data lives well past the policy that was supposed to remove it. A storage alert only fires once the disk is already a problem.

This blueprint reads the collection with a date filter, counts the documents older than the retention window and reports them. Behind a safe mode gate and an approval it deletes them with a single DELETE_MANY, then reads the collection a second time with the same filter and fails the run if any expired document is still there.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. The first run with the defaults only counts and reports. A delete needs auto_remediate true, dry_run false, then a human answering the Pause. Three conditions, all of them, before a document is removed.
  • The detect read and every re-check are capped at scan_limit, so a huge expired set cannot exhaust the worker. The delete is a server-side DELETE_MANY, so it is not capped and removes every matching document in one call.
  • The delete uses the same age filter as the detection step, so it touches only the documents that were counted. It never issues an unfiltered delete.
  • The re-read is the proof, not the delete count. If any document older than the window is still present after the delete, the run fails rather than reporting success.

How it works

  1. detect (io.kestra.plugin.mongodb.Find) selects documents whose timestamp_field is before the retention cutoff, oldest first, up to scan_limit. The cutoff is execution.startDate minus retention_days days, frozen for the whole run so detect, delete and the re-check all use one value.
  2. report (io.kestra.plugin.core.log.Log) logs the count. Task outputs are not in the execution JSON, so the number the gate acts on is written to the log.
  3. route (io.kestra.plugin.core.flow.If): zero expired documents logs clean and stops. One or more sends alert to Slack and prints planned, which states what a remediation run would do on every run including a dry one.
  4. remediate_gate (io.kestra.plugin.core.flow.If) opens only with auto_remediate true and dry_run false. hold (io.kestra.plugin.core.flow.Pause) waits for an answer.
  5. On approval delete (io.kestra.plugin.mongodb.Delete, DELETE_MANY) removes the documents, reverify (io.kestra.plugin.mongodb.Find) runs the same filter again and verify_gate fails the run with io.kestra.plugin.core.execution.Fail if the re-check still finds any. Without approval declined records the decision and nothing changes.
  6. errors posts to Slack when any task fails, for example when MongoDB is unreachable.

What you get

  • A count of the documents that are past the retention window, with the oldest one sampled in the log.
  • A Slack alert the moment expired documents exist, before storage becomes the symptom.
  • A gated, approved delete of only the expired documents, with the deleted count recorded.
  • A re-read that fails the run if the retention window is still not clean, so a half-done delete or a writer still inserting old-dated documents is caught rather than hidden.

Who it's for

  • Teams whose collection lost its TTL index or never had one, so it keeps growing.
  • SREs who want the retention policy enforced on a schedule with an audit trail per run.
  • Anyone who needs the delete to be provable, not assumed, after it runs.

Why orchestrate this with Kestra

A TTL index deletes documents quietly and gives no report, no approval step and no proof the policy held. This flow makes each of those explicit. The schedule decides when the policy is enforced, the detection count is logged, the destructive half sits behind auto_remediate, dry_run and a Pause with the decision recorded on the execution. The re-read turns the delete into a checked outcome. The Slack alert and the error path are the same ones the rest of your flows use.

Tested end to end

On Kestra 2.0.5 OSS against a MongoDB 7.0 container, on the final file. The collection was seeded with 5 documents dated 60 days old and 3 dated 5 days old, timestamp field createdAt. Every before and after number was read back with mongosh, not only from the Kestra log.

Run Inputs Result
Clean retention_days: 90 0 expired. clean logged, no Slack alert, SUCCESS. mongosh count still 8
Detect retention_days: 30 5 expired. alert to Slack, SUCCESS. mongosh confirms 5 older than the cutoff
Dry run retention_days: 30, defaults Reports 5, planned says it would delete, nothing deleted. mongosh count still 8
Armed but dry auto_remediate: true, dry_run: true Still no delete. mongosh count still 8
Declined auto_remediate: true, dry_run: false, resumed approved: false Paused, then declined. mongosh count still 8
Remediate auto_remediate: true, dry_run: false, resumed approved: true Deleted 5, re-check 0, SUCCESS. mongosh count 3, all recent
Still dirty the same, with a writer inserting old-dated documents during the run Delete ran, re-check found documents still expired, verify_gate failed the run and errors alerted
Unreachable connection_uri pointed at a dead host detect failed, errors posted to Slack, execution FAILED

Does the detector actually fail on a buildup

The clean run and the detect run isolate that. Nothing on the collection changed between them except the retention window: 90 days returned 0 expired, 30 days returned 5, on the same 8 documents. mongosh confirmed it directly with a $lt count against each cutoff. The remediate run then removed those 5 and the re-read returned 0. The still-dirty run proved the gate the other way, failing when documents older than the window remained.

Prerequisites

  • A reachable MongoDB instance and a user that can read the collection and run a delete on it.
  • A collection with a date field to age documents on. A TTL index is not required and is in fact the thing this flow stands in for when one is missing.
  • Local testing: docker run -d --name mdb --network mynet mongo:7.0, seed a mix of old and recent documents, then run the flow with connection_uri: mongodb://mdb:27017.

Secrets

  • MONGODB_URI: the MongoDB connection string. Read when connection_uri is left empty, which is how the flow should run in production. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_MONGODB_URI.
  • SLACK_WEBHOOK_URL: used by alert and the errors block. Provide it as SECRET_SLACK_WEBHOOK_URL.

Inputs

Input Default Purpose
connection_uri empty Plain connection string for a local test. Empty reads MONGODB_URI
database, collection appdata, events The collection to guard
timestamp_field createdAt The BSON date field documents are aged on
retention_days 30 Documents older than this many days are expired
scan_limit 1000 Ceiling on the detect read and each re-check
approval_timeout PT2H How long the pause waits for an answer
auto_remediate false Allow the delete
dry_run true Must be false for a delete to apply

Quick start

  1. Add the MONGODB_URI and SLACK_WEBHOOK_URL secrets.
  2. Set database, collection and timestamp_field, then run the flow. It reports the expired count and stops.
  3. To clear a buildup, set auto_remediate: true and dry_run: false, run it again, then answer the pause. The delete runs and the re-read proves the window is clean.
  4. Enable the daily schedule, which always runs in safe mode.

How to extend

  • Guard several collections by wrapping the tasks in a io.kestra.plugin.core.flow.Loop over a list, with concurrencyLimit: 1 on the part that deletes.
  • Replace the Pause with a Slack approval. Or drop it for a collection you are sure about.
  • Alert only when the expired count crosses a threshold, so a steady trickle stays quiet and a real buildup pages someone.
  • Once the collection is clean, create the TTL index the flow stands in for, so MongoDB keeps it clean between runs.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.