Cron Jobs & Heartbeats
Your job calls a URL when it finishes. If the call doesn't arrive on time, Zharp opens an incident.
A cron job monitor is passive: Zharp never connects to your server. The job sends a heartbeat — an HTTP request to its own ping URL — and Zharp works out when the next one is due. That makes it the right monitor for backups, imports, queue workers, scheduled CI runs and anything else that should happen on a schedule, wherever it runs, as long as it can make an outbound HTTPS request.
Create a cron job monitor
- 1Pick the type
On the Monitors page click New, open Monitor type and choose Cron job monitor.
- 2Copy the ping URL
The form shows a Ping endpoint box with the monitor's URL already reserved, so you can copy it before saving. It looks like this:
https://api.zharp.io/api/v1/ping/m_3f9c2a71d04be8a6
The last part is the monitor's token. Anyone holding the URL can send heartbeats for this monitor, so keep it out of public repositories — store it as a secret in CI.
- 3Tell Zharp the schedule
The expected schedule defaults to every 5 minutes. To change it, open Advanced settings and go to Cron settings — see Schedule and grace period.
- 4Save, then add the ping to your job
Click Create monitor, then add the URL to the end of the job — examples below. The first heartbeat is due one schedule after you save, plus the grace period, so the monitor waits for your job before it can be marked missed. You can find the full URL again by editing the monitor.
The ping URL
Send a GET or a POST to https://api.zharp.io/api/v1/ping/<token>. No API key or header is needed — the token in the path identifies the monitor. Each request records one successful run.
| Option | What it does |
|---|---|
?host=NAME | Names the machine the job ran on. Shown as Host in the run's details. An X-Host header does the same. |
| POST body | Kept as the run's output and shown under OUTPUT in its details — the last 4,000 characters. Send it as JSON (Content-Type: application/json); a plain-text body is not kept. |
Responses
200— the heartbeat was recorded. The JSON body'sdata.nextExpectedis the time the next heartbeat is due, grace period included.404— no monitor has that token. Check you copied the whole URL.
-f makes curl exit non-zero on an error response, -sS keeps it quiet unless something goes wrong, and --retry 3 rides out a brief network blip so a single dropped request doesn't become a missed run.
Start, failure and exit codes
The plain URL only says “the job ran”. Add a suffix to say more — each works with GET or POST, and takes ?host= and a body like the plain URL:
| URL | What it records |
|---|---|
/ping/<token>/start | The job has started. Nothing is checked yet; the next success or failure is timed from this, and its duration shows on the run. |
/ping/<token>/fail | The job failed. The monitor goes down straight away with Job failed with exit code 1 and an incident opens — it doesn't wait for the deadline. |
/ping/<token>/success | The job succeeded — the same as the plain URL. |
/ping/<token>/<exit code> | The job ended with this exit code, 0–255. 0 is a success; anything else is a failure with that code in the message. |
Anything else after the token answers 400 and records nothing.
A failed run shows as FAILED in Run history, apart from a MISSED one that never reported. Either way the next successful heartbeat brings the monitor back up.
#!/bin/sh PING_URL="https://api.zharp.io/api/v1/ping/YOUR_TOKEN" # Mark the start, so the run's duration is measured. curl -fsS --retry 3 "$PING_URL/start" > /dev/null /usr/local/bin/backup.sh code=$? # Report how it ended: 0 is a success, anything else a failure with that exit code. curl -fsS --retry 3 "$PING_URL/$code" > /dev/null
Schedule and grace period
Under Advanced settings → Cron settings, Expected schedule takes either a simple interval or a cron expression — switch with Use cron expression / Use a simple interval.
| Setting | Choices | Default |
|---|---|---|
| Expected schedule | Every n minutes, hours or days — or a standard five-field cron expression such as */5 * * * * | Every 5 minutes |
| Grace period | 1, 5, 10 or 30 minutes, or 1 hour | 5 minutes |
| Timezone | Any IANA timezone | UTC |
Each heartbeat sets the deadline for the next one:
- Interval: the next heartbeat is due one interval after this one arrived, plus the grace period. The clock starts from the last ping, not from the top of the hour.
- Cron expression: the next heartbeat is due at the expression's next run time after this ping, in the chosen Timezone, plus the grace period. Use this for schedules an interval can't express, such as weekdays only or 02:00 local time.
Set the grace period a little longer than the job usually takes. The ping comes at the end of the run, so a job that takes 20 minutes needs more than 5 minutes of grace. An expression that can't be parsed is rejected when you save, with Cron expression is not valid, e.g. */5 * * * *.
Examples
Replace YOUR_TOKEN with the token from your monitor, or paste the whole URL from the form.
# Run the backup every night at 02:00, then tell Zharp it finished. # && means the ping is only sent when the job exits 0. 0 2 * * * /usr/local/bin/backup.sh && curl -fsS --retry 3 https://api.zharp.io/api/v1/ping/YOUR_TOKEN > /dev/null
#!/bin/sh
# Stop at the first failing command, so a failed run never pings.
set -e
PING_URL="https://api.zharp.io/api/v1/ping/YOUR_TOKEN"
/usr/local/bin/backup.sh > /tmp/backup.log 2>&1
# Ping with the machine's name and the end of the job's log as the run's output.
tail -c 4000 /tmp/backup.log \
| jq -Rs '{output: .}' \
| curl -fsS --retry 3 -X POST \
-H "Content-Type: application/json" \
--data-binary @- \
"$PING_URL?host=$(hostname)"# .github/workflows/nightly.yml
on:
schedule:
- cron: "0 2 * * *"
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/nightly-report.sh
# Only reached when every step before it succeeded.
- name: Ping Zharp
run: curl -fsS --retry 3 "${{ secrets.ZHARP_PING_URL }}?host=github-actions"# Windows Task Scheduler: run the job, then ping only if it succeeded.
& C:\jobs\export.ps1
if ($?) { Invoke-RestMethod -Uri "https://api.zharp.io/api/v1/ping/YOUR_TOKEN" -Method Get | Out-Null }In these examples the ping only goes out when the job succeeded, so a failed job shows up like one that never ran: the monitor goes down once the grace period is over. To be told about a failure the moment it happens, report the exit code instead — see Start, failure and exit codes.
When it goes down
Zharp looks for overdue monitors every 10 seconds. When the deadline set by the last heartbeat has passed, it records a failed check whose message starts Heartbeat missed, marks the monitor down and opens an incident, which alerts the people and channels on the monitor.
The next heartbeat brings it back up and resolves the incident. On the monitor's page, Run history shows each run as SUCCESS, FAILED or MISSED, and the Schedule card shows the schedule, timezone and grace period it is checked against.