id: plivo-sms-delivery-failure-sentinel
namespace: company.team
description: |
Every hour, count recent outbound SMS in the Plivo Message API by final
state (read-only: no message is sent) and classify SMS delivery as
HEALTHY, WARNING, CRITICAL, LOW_VOLUME or UNREACHABLE. Log quiet runs and
alert Slack when messages start failing or going undelivered, with the
most common Plivo error code.
inputs:
- id: lookback_hours
type: INT
defaults: 2
displayName: "Lookback Window (hours)"
description: "Judge outbound SMS whose message_time is in the last this many hours."
- id: api_timezone
type: STRING
defaults: "UTC"
displayName: "Plivo Filter Time Zone"
description: "Time zone used to render the message_time__gt filter. Plivo
returns message_time with your account offset (for example +05:30); set
Asia/Kolkata if counts look shifted by that offset."
- id: warning_failure_rate
type: FLOAT
defaults: 3.0
displayName: "Warning Failure Rate (%)"
description: "At or above this share of failed plus undelivered SMS among
finished ones, the check is WARNING."
- id: critical_failure_rate
type: FLOAT
defaults: 10.0
displayName: "Critical Failure Rate (%)"
description: "At or above this share of failed plus undelivered SMS among
finished ones, the check is CRITICAL."
- id: min_messages
type: INT
defaults: 50
displayName: "Minimum Messages"
description: "Below this many finished SMS, the rate is too noisy to judge and
the run is LOW_VOLUME."
triggers:
- id: hourly_check
type: io.kestra.plugin.core.trigger.Schedule
description: Check SMS delivery every hour. Shipped disabled; enable it once the
Plivo secrets are set and a manual run has been verified.
cron: "10 * * * *"
timezone: Asia/Kolkata
disabled: true
tasks:
- id: count_delivered
type: io.kestra.plugin.core.http.Request
description: Count outbound SMS in the window that reached the handset
(message_state=delivered). Only meta.total_count is used. This is a GET;
no message is sent.
uri: "https://api.plivo.com/v1/Account/{{ secret('PLIVO_AUTH_ID')
}}/Message/?message_direction=outbound&message_type=sms&message_time__gt=\
{{ now() | dateAdd(-inputs.lookback_hours, 'HOURS') | date('yyyy-MM-dd
HH:mm:ss', timeZone=inputs.api_timezone) | urlencode
}}&message_state=delivered&limit=1"
method: GET
options:
auth:
type: BASIC
username: "{{ secret('PLIVO_AUTH_ID') }}"
password: "{{ secret('PLIVO_AUTH_TOKEN') }}"
allowFailed: true
timeout:
connectTimeout: PT10S
readIdleTimeout: PT60S
retry:
type: constant
interval: PT10S
maxAttempts: 3
allowFailure: true
- id: list_failed
type: io.kestra.plugin.core.http.Request
description: Count outbound SMS in the window that Plivo or the carrier rejected
(message_state=failed), and read up to 20 of them for their error codes.
This is a GET; no message is sent.
uri: "https://api.plivo.com/v1/Account/{{ secret('PLIVO_AUTH_ID')
}}/Message/?message_direction=outbound&message_type=sms&message_time__gt=\
{{ now() | dateAdd(-inputs.lookback_hours, 'HOURS') | date('yyyy-MM-dd
HH:mm:ss', timeZone=inputs.api_timezone) | urlencode
}}&message_state=failed&limit=20"
method: GET
options:
auth:
type: BASIC
username: "{{ secret('PLIVO_AUTH_ID') }}"
password: "{{ secret('PLIVO_AUTH_TOKEN') }}"
allowFailed: true
timeout:
connectTimeout: PT10S
readIdleTimeout: PT60S
retry:
type: constant
interval: PT10S
maxAttempts: 3
allowFailure: true
- id: list_undelivered
type: io.kestra.plugin.core.http.Request
description: Count outbound SMS in the window that the carrier accepted but
could not deliver (message_state=undelivered), and read up to 20 of them
for their error codes. This is a GET; no message is sent.
uri: "https://api.plivo.com/v1/Account/{{ secret('PLIVO_AUTH_ID')
}}/Message/?message_direction=outbound&message_type=sms&message_time__gt=\
{{ now() | dateAdd(-inputs.lookback_hours, 'HOURS') | date('yyyy-MM-dd
HH:mm:ss', timeZone=inputs.api_timezone) | urlencode
}}&message_state=undelivered&limit=20"
method: GET
options:
auth:
type: BASIC
username: "{{ secret('PLIVO_AUTH_ID') }}"
password: "{{ secret('PLIVO_AUTH_TOKEN') }}"
allowFailed: true
timeout:
connectTimeout: PT10S
readIdleTimeout: PT60S
retry:
type: constant
interval: PT10S
maxAttempts: 3
allowFailure: true
- id: summarize_messages
type: io.kestra.plugin.core.debug.Return
description: Add up delivered, failed and undelivered SMS from meta.total_count,
compute the failure share, find the most common error code among the
sampled failures, and classify in one jq query. Any missing or non-200
response becomes UNREACHABLE.
format: >-
{{ (outputs.count_delivered.code is not defined or
outputs.count_delivered.code != 200 or outputs.list_failed.code is not
defined or outputs.list_failed.code != 200 or
outputs.list_undelivered.code is not defined or
outputs.list_undelivered.code != 200) ?
'{"status":"UNREACHABLE","finished":0,"delivered":0,"failed":0,"undelivered":0,"rate":0,"top_error":"none","top_error_count":0}'
: (fromJson('{"d":' ~ outputs.count_delivered.body ~ ',"f":' ~
outputs.list_failed.body ~ ',"u":' ~ outputs.list_undelivered.body ~ '}')
| jq(' (.d.meta.total_count // 0) as $ok | (.f.meta.total_count // 0) as
$failed | (.u.meta.total_count // 0) as $undelivered | ($ok + $failed +
$undelivered) as $n | ($failed + $undelivered) as $bad | (if $n == 0 then
0 else (($bad * 1000 / $n) | floor) / 10 end) as $r | ([(.f.objects //
[])[], (.u.objects // [])[]] | map((.error_code // "unknown") | tostring |
split("\"") | join("") | .[0:40]) | group_by(.) | map({code: .[0], count:
length}) | sort_by(-.count)) as $errs | {finished: $n, delivered: $ok,
failed: $failed, undelivered: $undelivered, rate: $r, top_error: (if
($errs | length) > 0 then $errs[0].code else "none" end), top_error_count:
(if ($errs | length) > 0 then $errs[0].count else 0 end), status: (if $n <
' ~ inputs.min_messages ~ ' then "LOW_VOLUME" elif $r >= ' ~
inputs.critical_failure_rate ~ ' then "CRITICAL" elif $r >= ' ~
inputs.warning_failure_rate ~ ' then "WARNING" else "HEALTHY" end)} ') |
first | toJson) }}
- id: route_by_failures
type: io.kestra.plugin.core.flow.Switch
description: Route to a quiet log when delivery is healthy or volume is too low
to judge, a Slack message when failures cross the warning line, or a Slack
page when they cross the critical line or Plivo could not be read.
value: "{{ fromJson(outputs.summarize_messages.value).status }}"
cases:
"HEALTHY":
- id: log_healthy
type: io.kestra.plugin.core.log.Log
description: Log a quiet line when SMS delivery is healthy; no Slack message is
sent.
message: "Plivo SMS healthy: {{
fromJson(outputs.summarize_messages.value).failed }} failed and {{
fromJson(outputs.summarize_messages.value).undelivered }}
undelivered of {{
fromJson(outputs.summarize_messages.value).finished }} finished SMS
({{ fromJson(outputs.summarize_messages.value).rate }}%) in the last
{{ inputs.lookback_hours }} hour(s)."
"LOW_VOLUME":
- id: log_low_volume
type: io.kestra.plugin.core.log.Log
description: Log a quiet line when there is too little traffic in the window to
judge; no Slack message is sent.
message: "Plivo SMS: only {{ fromJson(outputs.summarize_messages.value).finished
}} finished outbound SMS in the last {{ inputs.lookback_hours }}
hour(s), below the minimum of {{ inputs.min_messages }}; rate not
judged this run."
"WARNING":
- id: alert_warning_sms
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Post a Slack message when the failure rate is at or above the
warning line.
url: "{{ secret('SLACK_WEBHOOK') }}"
payload: |
{
"text": "Plivo SMS WARNING: {{ fromJson(outputs.summarize_messages.value).failed }} failed and {{ fromJson(outputs.summarize_messages.value).undelivered }} undelivered of {{ fromJson(outputs.summarize_messages.value).finished }} finished SMS ({{ fromJson(outputs.summarize_messages.value).rate }}%) in the last {{ inputs.lookback_hours }} hour(s). Most common error code: {{ fromJson(outputs.summarize_messages.value).top_error }} (x{{ fromJson(outputs.summarize_messages.value).top_error_count }} in the sample). Execution {{ execution.id }}."
}
"CRITICAL":
- id: alert_critical_sms
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Page Slack when the failure rate is at or above the critical line,
which usually means a sender ID, a DLT template, the account balance
or a carrier route is broken.
url: "{{ secret('SLACK_WEBHOOK') }}"
payload: |
{
"text": "Plivo SMS CRITICAL: {{ fromJson(outputs.summarize_messages.value).failed }} failed and {{ fromJson(outputs.summarize_messages.value).undelivered }} undelivered of {{ fromJson(outputs.summarize_messages.value).finished }} finished SMS ({{ fromJson(outputs.summarize_messages.value).rate }}%) in the last {{ inputs.lookback_hours }} hour(s). Most common error code: {{ fromJson(outputs.summarize_messages.value).top_error }} (x{{ fromJson(outputs.summarize_messages.value).top_error_count }} in the sample). OTPs and alerts are not reaching customers; check the sender ID, templates, balance and the Plivo error code reference now. Execution {{ execution.id }}."
}
"UNREACHABLE":
- id: alert_unreachable_api
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Page Slack when the Message API could not be read, indicating an
outage or invalid credentials.
url: "{{ secret('SLACK_WEBHOOK') }}"
payload: |
{
"text": "Plivo SMS sentinel UNREACHABLE: the Message API did not return a usable response (HTTP delivered={{ outputs.count_delivered.code ?? 'none' }}, failed={{ outputs.list_failed.code ?? 'none' }}, undelivered={{ outputs.list_undelivered.code ?? 'none' }}). Check the auth ID and token. Execution {{ execution.id }}."
}
defaults:
- id: log_unclassified
type: io.kestra.plugin.core.log.Log
description: Fallback for any classification the cases above do not cover, so an
unexpected value is visible in the logs.
message: "Plivo SMS sentinel completed with an unexpected classification: {{
outputs.summarize_messages.value }}."
- id: log_audit_result
type: io.kestra.plugin.core.log.Log
description: Record one audit line for every run, whichever branch fired, so the
delivery history stays queryable even if Slack delivery fails.
message: "Plivo SMS sentinel audit: lookback_hours={{ inputs.lookback_hours }}
http={{ outputs.count_delivered.code ?? 'n/a' }}/{{
outputs.list_failed.code ?? 'n/a' }}/{{ outputs.list_undelivered.code ??
'n/a' }} summary={{ outputs.summarize_messages.value }} execution={{
execution.id }}."
errors:
- id: alert_flow_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Alert Slack when the sentinel flow itself fails, so a broken check
is never mistaken for healthy delivery.
url: "{{ secret('SLACK_WEBHOOK') }}"
payload: |
{
"text": "Plivo SMS sentinel flow {{ flow.id }} FAILED (execution {{ execution.id }}). SMS delivery health is now unknown; check the task logs and the Plivo message logs."
}
outputs:
- id: status
type: STRING
value: "{{ fromJson(outputs.summarize_messages.value).status }}"