New to Kestra?
Use blueprints to kickstart your first workflows.
Archive old Postgres rows to Parquet in MinIO/S3, verify by read-back, and purge only after a typed human confirmation in Kestra.
Deleting old rows from a hot Postgres table is usually a manual "export, then DELETE" with no proof that the export is readable. One bad dump means permanent data loss, and nobody can say afterwards what was removed.
This blueprint makes the proof and the approval part of the pipeline:
retention_days to Parquet and uploads it to MinIO/S3.Rows are deleted only if archived rows, exported rows and the rows still in Postgres at purge time all agree.
archive_request (io.kestra.plugin.core.trigger.Webhook) starts the flow; the body fields table, date_column and retention_days map onto the flow inputs.count_candidates (io.kestra.plugin.jdbc.postgresql.Query) counts rows older than the cutoff, and has_rows (io.kestra.plugin.core.flow.If) ends the run early when there is nothing to archive.export_rows (io.kestra.plugin.jdbc.postgresql.Query) streams the rows to internal storage, to_parquet (io.kestra.plugin.serdes.parquet.IonToParquet) converts them, and upload_archive (io.kestra.plugin.minio.Upload) writes the object to the bucket.download_back (io.kestra.plugin.minio.Download) reads the object back and count_archive (io.kestra.plugin.jdbc.duckdb.Query) counts its rows; verify_archive (io.kestra.plugin.core.execution.Assert) fails the run if the counts differ.ask_slack (io.kestra.plugin.core.flow.If) optionally sends the approval request through notify_approval (io.kestra.plugin.slack.notifications.SlackIncomingWebhook).approve_purge (io.kestra.plugin.core.flow.Pause) waits for a human to type the table name, up to two days. If nobody answers, the execution is cancelled and nothing is deleted.confirm_name and still_matches (io.kestra.plugin.core.execution.Assert) check the typed name and that a fresh recount still equals the archived count.purge (io.kestra.plugin.jdbc.postgresql.Query) deletes the rows and verify_purge checks the deleted count against the archived count.DELETE, and the archive stays in the bucket.A shell script can export and delete, but it cannot pause for approval, verify the stored object independently, or leave a per-run audit trail. Kestra gives the flow a webhook entry point, a Pause that survives restarts, assertions that stop the run on a mismatch, and a history of every execution with the exact counts at each step.
cold-archive) that already exists.POSTGRES_URL: JDBC URL of the source database, for example jdbc:postgresql://host:5432/db.POSTGRES_USERNAME: database user with read and delete rights on the table.POSTGRES_PASSWORD: password for that user.MINIO_ENDPOINT: endpoint URL of the MinIO or S3-compatible store.MINIO_ACCESS_KEY_ID: access key for the bucket.MINIO_SECRET_KEY_ID: secret key for the bucket.ARCHIVE_WEBHOOK_KEY: key segment that secures the webhook URL.SLACK_WEBHOOK_URL: Slack incoming webhook, only needed when notify_slack is true.table (STRING, default events): lowercase unquoted table name, regex-validated because it is interpolated into SQL.date_column (STRING, default created_at): timestamp or date column used for the cutoff, same validation.retention_days (INT, default 365, minimum 30): rows older than this are archived, then purged.bucket (STRING, default cold-archive): destination bucket.notify_slack (BOOL, default false): send the approval request to Slack.curl -X POST "https://kestra.example.com/api/v1/main/executions/webhook/company.team/postgres-verified-cold-archive/<ARCHIVE_WEBHOOK_KEY>" \
-H "Content-Type: application/json" \
-d '{"table":"events","date_column":"created_at","retention_days":365}'
approve_purge, then resume it and type the table name.log_done for Archived and purged N rows ... and find the object at s3://<bucket>/archive/<table>/cutoff=<date>/<execution id>.parquet.A paused run holds the flow's concurrency slot until it is resumed or times out.
Subflow and call it once per table.archive/ to expire old archives.minio for aws.s3 or gcp.gcs uploads and downloads.