New to Kestra?
Use blueprints to kickstart your first workflows.
Schedule a daily PostgreSQL pipeline rollup in Kestra and post a backfill-safe digest to Zulip, with retry and a failure alert.
This flow turns per-run pipeline noise into a single readable morning summary. A Schedule trigger fires once a day, a PostgreSQL Query rolls up the previous day's runs from a pipeline_runs audit table (run count, rows processed, failed records, average runtime, and an overall status), and ZulipIncomingWebhook posts a formatted markdown message to a Zulip channel for the data team. If the rollup or the post fails, a flow-level errors handler posts a loud "digest FAILED" notice to the same channel so a missing summary is never silent.
The query targets a generic analytics.pipeline_runs audit table with columns pipeline_name, run_date, rows_processed, failed_records, and runtime_seconds. Point it at whatever runs/observability table you already populate (or a dbt run_results export); the digest payload references the aggregated row, so only the SQL needs to change to fit your schema.
compute_summary carries a retry (constant, three attempts) so a transient database or network blip does not kill the morning report on the first try.errors block posts a distinct failure message that includes the run date and execution id, so the data team can always tell "pipeline healthy and quiet" apart from "the digest flow broke."concurrency: { limit: 1 } ensures a backfill or a catch-up after downtime cannot post several conflicting digests for the same window at once.ZulipIncomingWebhook does not raise on a non-200 response, so even with the retry, delivery of the digest itself is best-effort; check the task logs if a message does not appear.The data window is derived from {{ (trigger.date ?? execution.startDate) | date('yyyy-MM-dd') }} and bound into the SQL as the :run_date prepared-statement parameter, and the SAME value is rendered into the message. A backfilled or manually started run therefore reports the historical day it is replaying, not "today," and the partition shown in the message always matches the data queried.
The payload prefixes a :white_check_mark: when status is healthy and a :x: when status is degraded, so an unhealthy day is visually distinct from a healthy one rather than blending in. All interpolated values pass through | toJson so a stray quote or newline in any field cannot break the JSON payload.
io.kestra.plugin.core.trigger.Schedule runs the flow daily at 07:00 in the Europe/Paris timezone.compute_summary runs the rollup with fetchType: FETCH_ONE, exposing the aggregated figures as {{ outputs.compute_summary.row.* }}.post_digest renders those values into a markdown payload and POSTs it to the Zulip incoming webhook.analytics.pipeline_runs).POSTGRES_URL, POSTGRES_USERNAME, POSTGRES_PASSWORD: connection to the audit database, set once via pluginDefaults for the io.kestra.plugin.jdbc.postgresql group.ZULIP_WEBHOOK: the full Zulip incoming webhook URL, including the integration path and API key, in the form https://yourZulipDomain.zulipchat.com/api/v1/external/INTEGRATION_NAME?api_key=API_KEY. Used by both ZulipIncomingWebhook tasks.ZULIP_WEBHOOK secret.pluginDefaults block to your warehouse plugin (for example io.kestra.plugin.jdbc.snowflake).sql to your audit table and the cron/timezone to your reporting window, keeping the :run_date and :pipeline_name bindings.{{ outputs.compute_summary.row.* }} holds the rollup figures (run_count, rows_processed, failed_records, runtime_min, status) injected into the message.ZulipIncomingWebhook is a notification task and produces no internal-storage artifact, so there is no {{ outputs.post_digest.uri }}. Success is a message in the Zulip channel plus a SUCCESS state on post_digest.