Query icon
Return icon
Switch icon
Log icon
SlackIncomingWebhook icon
If icon
Loop icon
Delete icon
Fail icon
Schedule icon

Cosmos DB Expired Document Retention Gate

Audit Cosmos DB containers for documents past their own expiry field, alert Slack on retention violations, and optionally delete and verify.

Categories
DataInfrastructure

Cosmos DB's container-level TTL deletes documents in the background, on its own schedule, and only if it is configured at all. An application that stamps every document with its own expiresAt field and assumes TTL will clean it up has no guarantee that actually happens, TTL can be disabled on the container, misconfigured, or simply lag under load, and the document, its PII, its stale state, just stays there. This blueprint turns that assumption into a Day 2 governance gate: it queries for documents whose own expiresAt field has passed, classifies the real count it finds, alerts Slack, and can delete the stragglers itself, then proves they are actually gone.

How it works

  1. probe_violations (io.kestra.plugin.azure.storage.cosmosdb.Query) runs a cross-partition SQL query selecting id, partition_key_field, and expiry_field for documents where expiresAt is defined and older than the current UTC timestamp minus grace_period_minutes.
  2. classify_retention_status (io.kestra.plugin.core.debug.Return) maps the count of returned documents to COMPLIANT (0), WARNING (1 to threshold - 1), CRITICAL (at or above violation_count_critical_threshold), or PROBE_FAILED if the query task failed to produce outputs.
  3. route_by_retention_status (io.kestra.plugin.core.flow.Switch) routes based on that status: COMPLIANT logs an audit line, while WARNING, CRITICAL, and PROBE_FAILED post structured alerts to Slack.
  4. remediate_gate (io.kestra.plugin.core.flow.If) activates only when violations were found, auto_remediate is true, dry_run is false, and remediation_action is delete. It calls io.kestra.plugin.azure.storage.cosmosdb.Delete once per violating document, supplying both id and the partition key value, runs reprobe_violations to ensure zero lingering documents remain, and fails the execution via io.kestra.plugin.core.execution.Fail if any expired documents are still detected.
  5. log_audit_summary (io.kestra.plugin.core.log.Log) records the final retention gate outcome across all paths.
  6. alert_flow_failure (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) in the errors block captures unhandled flow-level errors.

What you get

  • Active enforcement of document retention SLAs rather than passive assumption that container TTL purged records.
  • Tiered severity classification (WARNING vs CRITICAL) based on violation volume.
  • Safe-by-default remediation: shipping in dry_run: true and log_only mode with explicit post-delete verification.
  • Zero external scripts: leverages official plugin-azure tasks directly.

Who it's for

  • Data compliance and security teams enforcing GDPR/CCPA data retention requirements.
  • SRE and platform engineers operating Azure Cosmos DB workloads who need assurance that data past its retention window is actually gone.
  • Teams evaluating io.kestra.plugin.azure.storage.cosmosdb.Query and Delete together.

Why orchestrate this with Kestra

Cosmos DB's own TTL has no concept of an application-level expiry field, a violation threshold, or an alert explaining what it found. Kestra adds the schedule, the WARNING versus CRITICAL classification built from the query's own row count, the Slack alert, the gated delete, and the verification that the delete worked, all as one declarative YAML file you can version and review like code.

Prerequisites

  • A Cosmos DB account (or the local emulator, see below) with a database and container whose documents carry an expiry_field such as expiresAt.

  • Local testing with the official Linux Cosmos DB emulator, no Azure account needed:

    docker run --detach --name cosmos-emulator \\
      --publish 8081:8081 --publish 8082:8080 --publish 1234:1234 \\
      mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-latest --protocol https
    

    This remaps the emulator's health probe from its default host port 8080, which otherwise collides with Kestra's own default UI port, to 8082. The real Cosmos gateway stays on 8081, and the Data Explorer stays on 1234.

  • This plugin calls the Cosmos Java SDK directly from the Kestra worker process. If Kestra itself runs in a container, point COSMOS_CONNECTION_STRING at https://host.docker.internal:8081/.

  • The Cosmos Java SDK requires HTTPS against the emulator (--protocol https above). Export and trust the emulator's self-signed certificate in the JVM running your Kestra worker.

  • A Slack incoming webhook for alerts.

Secrets

  • AZURE_TENANT_ID: Azure AD Tenant ID (GUID), required by Kestra's Azure identity task model.

  • COSMOS_CONNECTION_STRING: AccountEndpoint=<endpoint>;AccountKey=<key>; format, used by probe_violations, reprobe_violations, and delete_expired_document.

  • SLACK_WEBHOOK: Slack incoming webhook URL used by the alert tasks and the errors block.

    In Kestra OSS, set these as base64-encoded environment variables: SECRET_AZURE_TENANT_ID, SECRET_COSMOS_CONNECTION_STRING, SECRET_SLACK_WEBHOOK.

Inputs

Input Type Default Allowed values
database_id STRING compliance_db any Cosmos database name
container_id STRING customer_records any Cosmos container name
partition_key_field STRING partitionKey document property used as partition key
expiry_field STRING expiresAt document field name holding an ISO-8601 timestamp
grace_period_minutes INT 60 any non-negative integer
violation_count_critical_threshold INT 50 any non-negative integer
remediation_action SELECT log_only log_only, delete
auto_remediate BOOL false true, false
dry_run BOOL true true, false

Quick start

  1. Start the Cosmos emulator (or point at a real account) and seed a container with a few documents carrying a past expiresAt timestamp.
  2. Add the AZURE_TENANT_ID, COSMOS_CONNECTION_STRING, and SLACK_WEBHOOK secrets.
  3. Run this flow manually with the defaults and read log_audit_summary for the result.
  4. Once you trust the query, set remediation_action: delete, auto_remediate: true, and dry_run: false and run it again to clean up and verify.
  5. Enable the six_hourly trigger, which always ships in safe mode.

How to extend

  • Point this at a container partitioned on any property by configuring partition_key_field (e.g., tenantId, region, or id).
  • Add a second expiry_field and a second probe/classify/switch chain to audit more than one retention policy per container in one flow.
  • Replace the fixed violation_count_critical_threshold with a comparison against yesterday's count stored in a Kestra KV entry, to catch a retention backlog that is growing rather than a static size.

Pitfalls

  • Cosmos DB tasks inherit from AbstractAzureIdentityConnection, which marks tenantId with @NotNull. Even when connecting via COSMOS_CONNECTION_STRING, tenantId: "{{ secret('AZURE_TENANT_ID') }}" must be specified on each task to satisfy Kestra 2.0.1 schema validation.
  • io.kestra.plugin.azure.storage.cosmosdb.Delete requires both id and the partition key value in item. The probe query explicitly selects partition_key_field so delete_expired_document can pass both fields to the Cosmos DB SDK.
  • The Cosmos Java SDK does not support HTTP against the emulator, start it with --protocol https and trust its certificate, or every probe reports PROBE_FAILED with a TLS error that looks identical to a real outage.
  • IS_DEFINED(c.<expiry_field>) matters: without it, a document that never had the field set at all compares undefined < "<timestamp>", which Cosmos SQL does not treat as true, so those documents are silently excluded rather than counted.
  • grace_period_minutes should be at least as large as your container's configured TTL polling interval, otherwise this gate reports violations for documents Cosmos's own TTL was always going to delete on schedule.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.