New to Kestra?
Use blueprints to kickstart your first workflows.
Audit a JMS dead-letter queue by redelivery count, classify poison message quarantine status, and alert Slack on breach.
A dead-letter queue filling up with ordinary failures is expected; a dead-letter queue filling up with the same poison messages over and over is a different, more urgent problem; the two look identical unless you check how many times each message has already been redelivered. This blueprint uses plugin-jms's native Consume task as the probe, filtering server-side with a JMS message selector on the standard JMSXDeliveryCount property so only genuinely poisoned messages, those redelivered past a threshold, count toward the alert.
audit_poison_messages (io.kestra.plugin.jms.Consume) connects to dlq_destination_name via a DIRECT connectionFactoryConfig, applies the message selector JMSXDeliveryCount > poison_redelivery_threshold, and pulls up to max_messages_per_audit matches within max_wait_timeout_ms. destinationType is hard-coded to QUEUE: it is a fixed Java enum on the task, not a Property<String>, so Kestra cannot render a Pebble expression into it, and dead-letter destinations are a point-to-point consume pattern in practice, never a pub-sub TOPIC. allowFailure: true lets the flow continue instead of aborting if the broker is unreachable or the provider JAR cannot be loaded.classify_quarantine_status (io.kestra.plugin.core.debug.Return) checks whether outputs.audit_poison_messages.count is defined at all. If it is missing, the classification is PROBE_FAILED. If it is defined and at or above poison_alert_threshold, the classification is POISON_QUARANTINE_BREACH; otherwise HEALTHY.route_by_quarantine_status (io.kestra.plugin.core.flow.Switch) branches on that string. POISON_QUARANTINE_BREACH runs evaluate_alert_toggle (io.kestra.plugin.core.flow.If), which pages Slack via dispatch_poison_alert only when send_slack_alert is true, logging a suppressed-alert line otherwise. HEALTHY logs a clean audit line. PROBE_FAILED pages Slack directly since the quarantine state is now unknown. The defaults branch logs a fallback line that is unreachable under normal operation.log_audit_summary records a status line on every run regardless of branch, so execution history reads as a continuous poison-message trend.errors block pages Slack separately if the sentinel flow itself fails outright.jms_poison_audit_schedule trigger polls every 6 hours with explicit input overrides, shipped disabled: true until pointed at a real broker and destination.status output (POISON_QUARANTINE_BREACH / HEALTHY / PROBE_FAILED) other flows can gate on.JMSXDeliveryCount so the alert count reflects genuinely stuck messages, not every routine dead-letter arrival.outputs.audit_poison_messages.uri) containing the matched poison message batch for manual inspection.A one-off JMX console check or a custom cron script proves the dead-letter count for one instant and loses the redelivery-count context. This flow keeps every audit in Kestra's execution history, turns the result into a typed status output other flows can depend on, stores the matched poison batch in internal storage for inspection, and gives you an If/Switch branch to grow into real remediation (reprocessing, ticketing) without rewriting the probe itself.
JMSXDeliveryCount property on redelivery).jms-libs folder on the Worker, or uploaded as a Kestra namespace file and referenced via provider_jar_path.JMS_USERNAME, JMS_PASSWORD, and SLACK_WEBHOOK secrets.dlq_destination_name, jms_broker_url, and jms_connection_factory_class at your real broker and dead-letter destination.status reads HEALTHY or POISON_QUARANTINE_BREACH as expected for your current redelivery counts.jms_poison_audit_schedule.outputs.audit_poison_messages.uri to group poison messages by headers (JMS properties) such as originating queue or error type.status into a KV store to alert only on state transitions instead of every run.Loop task to monitor multiple services from one flow.io.kestra.plugin.jms.Produce task to republish selectively reprocessed messages back onto their source destination after manual review.JMS_USERNAME: Broker username with consume permission on the dead-letter destination.JMS_PASSWORD: Broker password for that user.SLACK_WEBHOOK: Slack incoming webhook used for the breach, probe-failure, and flow-failure alerts.SECRET_JMS_USERNAME, SECRET_JMS_PASSWORD, and SECRET_SLACK_WEBHOOK as base64, for example echo -n "<your-value>" | base64.jms_broker_url (STRING, default tcp://localhost:61616): broker connection URL.jms_connection_factory_class (STRING, default org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory): ConnectionFactory implementation class.provider_jar_path (STRING, default kestra:///jms/artemis-client.jar): provider client JAR location.dlq_destination_name (STRING, default ActiveMQ.DLQ): dead-letter destination to audit. Always treated as a QUEUE, see Pitfalls.message_serde_type (SELECT, one of STRING, JSON, BYTES, default STRING): message body deserializer.poison_redelivery_threshold (INT, default 3): JMSXDeliveryCount floor used in the selector to define poison.max_messages_per_audit (INT, default 50): maximum matching messages pulled per audit pass.max_wait_timeout_ms (INT, default 5000): bounded poll time in milliseconds.poison_alert_threshold (INT, default 5): matched count at or above which the destination is classified POISON_QUARANTINE_BREACH.send_slack_alert (STRING, default "true"): when "true", page Slack on breach; when "false", only log it.{{ outputs.audit_poison_messages.count }}: number of matched poison messages in this pass.{{ outputs.audit_poison_messages.uri }}: internal storage URI (kestra://) containing the matched message batch.{{ outputs.classify_quarantine_status.value }}: classification string, one of POISON_QUARANTINE_BREACH, HEALTHY, PROBE_FAILED.{{ outputs.status }}: flow-level alias of the same classification.destinationType on io.kestra.plugin.jms.Consume is a fixed enum (QUEUE or TOPIC), not a Property<String>, so Kestra cannot render a Pebble input into it; a templated {{ inputs.destination_type }} fails deserialization before the flow ever runs. This blueprint hard-codes QUEUE. If you need to audit a TOPIC-backed dead-letter destination, branch with an io.kestra.plugin.core.flow.If into two separate Consume tasks, one with destinationType: QUEUE and one with destinationType: TOPIC, each hard-coded.maxWaitTimeout defaults to 0 inside plugin-jms, which blocks indefinitely waiting for a match. This blueprint always sets max_wait_timeout_ms explicitly; do not clear it to 0 in a scheduled flow.JMSXDeliveryCount is a standard optional JMS property; it is populated by ActiveMQ Classic and Artemis on redelivery, but a non-compliant or misconfigured provider may not set it, which would make every message match or none at all. Verify the property is present before trusting the threshold.name; the real field verified against the plugin's Java source and test suite is destinationName. Use destinationName, not name.provider_jar_path or under the plugin's jms-libs folder; a missing or mismatched JAR surfaces as a connection failure, which this flow classifies as PROBE_FAILED rather than HEALTHY.connectionFactoryClass must match the broker you actually run; the Artemis and ActiveMQ Classic connection factory classes are not interchangeable.To start a local Apache ActiveMQ Artemis broker container for testing, execute:
docker run --detach --name kestra-artemis -p 61616:61616 -p 8161:8161 apache/activemq-artemis:latest-alpine
The default web console credentials are username and password both set to artemis; override them with -e ARTEMIS_USER=
If Kestra is running inside a Docker network, connect the broker container to Kestra's network:
docker network connect YOUR_KESTRA_NETWORK kestra-artemis
(Run docker network ls to verify your active Kestra network name.)
