Exit codes and the warn state
Report the exit code with the ping and PulseWatch decides what it means, using rules you control. Without this there are two answers, worked and did not work, and most jobs have a third: it finished, and something was wrong. A backup that skipped four locked files is neither a clean run nor an outage, and filing it as either is how a real problem goes unnoticed for a month.
Reporting the code
Three ways, all equivalent. The path form is the one to reach for.
| Form | Example |
|---|---|
| Path segment | /ping/<token>/exit/100 |
| Query parameter | /ping/<token>?exit=100 |
| JSON body | {"exit_code": 100} |
Valid codes are 0 to 255. A malformed value is dropped rather than failing the ping, so /exit/ with nothing after it is treated as a plain ping. If your rule seems to be ignored, check the code actually arrived.
Setting it up
- Create a heartbeat monitor and copy its ping URL. Nothing here needs a paid plan; the defaults below apply to every account.
- Change the job to report its code. The important detail is the separator:
;runs the curl whatever happened,&&runs it only on success, which means a failing job sends nothing and shows up as silence.bash# The whole change: report the code instead of only reporting success. # Note the ; not &&. With &&, a failing job sends nothing at all. 0 3 * * * /opt/backup.sh; curl -fsS -m 10 --retry 5 \ "https://pulsewatcher.up.railway.app/ping/<token>/exit/$?"
- Let it run once. On the monitor page the run now reads
exit 0 (ok)rather than just success. - Open the Exit codes card on the monitor page. This is a card of its own, below the metric rules, not a field inside the monitor form.
- Add a rule: from and to (both 100 for a single code), an outcome, and a label. The label is what the alert says, so write the meaning rather than repeating the number.
- Confirm it worked: send a test ping with that code and watch the run. It should show your label and the outcome you chose.Rules are cached for up to a minute on the ping path, so an edit can take one more ping to take effect.bash
curl -fsS "https://pulsewatcher.up.railway.app/ping/<token>/exit/100"
Worked example: a backup that skips locked files
The job exits 100 when it completed but had to skip something.
#!/usr/bin/env bash
set -uo pipefail
skipped=0
for f in /data/*.sql; do
if ! restore "$f"; then
skipped=$((skipped + 1))
fi
done
if [ "$skipped" -gt 0 ]; then
echo "skipped $skipped locked files" >&2
exit 100 # completed, with problems
fi
exit 0With the default rules and no configuration at all, 100 is already warn with the label completed with issues. Adding a rule of your own changes the label to something specific:
from 100 to 100 outcome warn label "skipped locked files"
The monitor stays up, no incident opens, nobody is paged, and the next morning the digest says:
Subject: Nightly restore: 3 warnings in the last 24 hours 3 in the last 24 hours. Nothing here took the monitor down. 03:00 exit 100 (skipped locked files): warn 04:00 exit 100 (skipped locked files): warn 05:00 exit 100 (skipped locked files): warn
The defaults you already have
Every account starts with a set of rules covering the conventions. You only add a rule where your job means something different.
| Codes | Outcome | Label |
|---|---|---|
| 0 | success | ok |
| 1 to 63 | down | failed |
| 64 to 78 | down | sysexits.h, named individually: usage error, data format error, cannot open input, and so on |
| 100 to 119 | warn | completed with issues |
| 124, 125 | down | timeout (GNU timeout) and timeout command failed |
| 126, 127 | down | command found but not executable, command not found |
| 129 to 165 | down | killed by signal, with 137 named as SIGKILL, likely out of memory |
The 100 to 119 range is the recommended convention. Changing exit 1 to exit 100 in a script is the cheapest way to start using the warn state, and it needs no configuration at all.
What each outcome does
| Outcome | Means | Effect |
|---|---|---|
| success | The run was clean | Nothing is sent. The monitor is up. |
| warn | The run completed, with problems | Never pages and never opens an incident. Counts as available, because the work got done. Delivered on the warn schedule. |
| down | The run failed | Pages, opens an incident, repeats if you set a repeat interval. |
| ignore | This code carries no information | The ping falls back to what it already said: a plain ping stays success, a /fail ping stays down. Use it to silence a code your wrapper emits for its own reasons. |
Which rule wins
Rules can live on a monitor, on a fleet, or on the account as a default. When several match a code, the most specific one wins:
- A rule scoped to this monitor beats an account default.
- Among equally scoped rules, the narrower range wins.
That is why the broad 129 to 165 killed by signal rule and the specific 137 SIGKILL, likely out of memory rule coexist without either knowing about the other. Exit 137 gets the OOM label; exit 140 gets the general one.
A non-zero code on a plain success ping is treated as authoritative, and the code wins. A wrapper that pings unconditionally would otherwise file every failed run as a success.
How warnings are delivered
A warn never pages. What varies is when you hear about it.
| warn_delivery | Behaviour |
|---|---|
| digest_daily | The default. One summary per monitor per day, listing the warnings since the last one. A day with no warnings sends nothing, and a quiet day does not consume the window, so the first warning after a quiet spell is not delayed another day. |
| immediate | One message per warning, as it happens. Still never pages, still opens no incident. |
| silent | Recorded on the monitor page and in the run history, never sent anywhere. |
A warning that never stops becomes an alert
warn_escalate_after defaults to 3 and takes a value from 1 to 100. After that many consecutive warns the monitor goes down with reason persistent_warnings, and that one does page and does open an incident.
03:00 exit 100 up -> warn digest, no page
04:00 exit 100 warn -> warn digest, no page
05:00 exit 100 warn -> down PAGES, opens an incident
reason: persistent_warningsWithout escalation, warn would be a way to make a real failure permanently invisible. A backup that has skipped files every night for three weeks is not having a good month.
Every field
| Field | Type | Range | Effect |
|---|---|---|---|
| code_min | int | 0 to 255 | Start of the range this rule matches. |
| code_max | int | 0 to 255 | End of the range. Set both to the same number for a single code. |
| outcome | enum | success, warn, down, ignore | What the code means. |
| label | text | up to 80 characters | Appears in the alert, so it reads "exit 100 (skipped locked files)" rather than "exit 100". |
| scope | owner | monitor, fleet, or account default | A rule with no monitor and no fleet applies to everything on the account that has no more specific rule. |
Exit-code rules are available on every plan, including Free. warn_delivery and warn_escalate_after are fields on the monitor, documented with the rest of the monitor configuration.
Variations
Nothing to configure
Use exit 100 in your scripts and stop. The defaults already treat it as a warning.
Codes that mean specific things
100 skipped files, 101 partial sync, 102 upstream unavailable, each with its own label. The alert then names the problem rather than a number.
A code that should page
Map it to down. Data corruption detected is not a warning even if the job exited politely.
Silencing a wrapper
Some runners exit non-zero for reasons of their own. Map that code to ignore and the ping falls back to what it already said.
An account-wide convention
Set the rules with no monitor selected and they apply everywhere, including to monitors created later. Override per monitor where a job disagrees.
What can go wrong
The job failed and PulseWatch heard nothing
The line uses &&, so the ping only runs on success. Use ; with /exit/$?.
$? is always 0
Something ran between the job and the curl and reset it. Capture it immediately: job.sh; code=$?; curl .../exit/$code.
A pipeline hides the failure
job.sh | tee log reports tee's exit code, not the job's. Use set -o pipefail, or read ${PIPESTATUS[0]}.
The rule appears to be ignored
Either a narrower rule matched first, or the code never arrived, or the edit is inside the one-minute cache window. Check the run: it shows the code it actually received.
Warnings are arriving as pages
Consecutive warns crossed warn_escalate_after. That is the escalation working. Fix the underlying warning, or raise the threshold if three in a row is genuinely normal for that job.
Related
- Metric rules when the job exits 0 and the number it produced is wrong. An exit code says how the run ended; a metric says what it did.
- Reporting errors to send the stack trace along with the failure.
- States and reasons for exactly what warn does to uptime and incidents.
- Fleets to declare one set of exit-code rules for hundreds of machines.