New to Kestra?
Use blueprints to kickstart your first workflows.
Audit Cosmos DB containers for documents past their own expiry field, alert Slack on retention violations, and optionally delete and verify.
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.
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.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.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.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.log_audit_summary (io.kestra.plugin.core.log.Log) records the final retention gate
outcome across all paths.alert_flow_failure (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) in the
errors block captures unhandled flow-level errors.WARNING vs CRITICAL) based on violation volume.dry_run: true and log_only mode with explicit
post-delete verification.plugin-azure tasks directly.io.kestra.plugin.azure.storage.cosmosdb.Query and Delete together.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.
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.
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.
| 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 |
expiresAt timestamp.AZURE_TENANT_ID, COSMOS_CONNECTION_STRING, and SLACK_WEBHOOK secrets.log_audit_summary for the result.remediation_action: delete, auto_remediate: true, and
dry_run: false and run it again to clean up and verify.six_hourly trigger, which always ships in safe mode.partition_key_field
(e.g., tenantId, region, or id).expiry_field and a second probe/classify/switch chain to audit more than
one retention policy per container in one flow.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.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.--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.