Skip to content
← Guides

Your scheduled job "succeeded" but the output was empty. Here's how to catch it.

Exit code 0 only means the job didn't crash. It doesn't mean it did anything. To catch empty runs, each run has to report what it produced, and something outside the job has to check it.

Short answer

Have the job send a measure of its output when it checks in (an item count, the output text, or a marker word), set a minimum, and alert when a run falls short. Did It Run? does this for cron jobs, scripts, n8n, Zapier, Make, GitHub Actions and AI agents: each run pings its watch's URL when it finishes, and if the check-in reports fewer items than your minimum, is smaller than your minimum size, or is missing your required keyword, you get an empty alert. If no check-in arrives at all, you get a missed alert. Start free (3 watches, no credit card) or follow the cron recipe.

Why "success" can be empty

  • The query matched nothing. A date filter in the wrong timezone, an upstream table that hadn't loaded yet, a renamed column. The job runs, finds 0 rows, and exits cleanly.
  • The API said 200 with nothing in it. An empty list, an expired token reported in the body instead of the status code, a changed response shape.
  • The error was swallowed. || true, an empty catch {}, a "continue on error" setting, or a retry wrapper that gives up quietly.
  • A pipeline hid the failure. In bash, pg_dump mydb | gzip > backup.gz returns the exit status of gzip, so it can "succeed" with an empty backup if pg_dump failed. set -o pipefail makes the pipeline fail instead (see the Bash manual on pipelines).
  • The AI agent answered, but not with the work. A refusal, "I couldn't access that page", or a summary of zero items. To the scheduler it's a finished run.

Error alerts and exit codes can't see any of these. A plain heartbeat can't either: the ping arrives on time, so it looks healthy.

Step 1: decide what "not empty" means for this job

Pick the simplest signal that's always true on a good day:

  • A count: at least 1 row exported, at least 1 new lead, at least 5 items in the digest.
  • A size: a report or backup is never smaller than a few hundred bytes.
  • A marker: a word that only appears in a real result. For AI agents, ask them to end every answer with a fixed word such as DIGEST-OK; a refusal or an error message won't contain it.

Step 2: report it in the check-in

Send the count or the output with the success ping. Did It Run? reads it from the request:

  • Item count in the URL: https://diditrun.app/p/<your-token>?items=12
  • JSON body: {"items": 12, "text": "Exported 12 rows"}. The count can also be sent as count, rows or n, and the text as output, summary or message. If you POST a JSON array, its length is used as the item count.
  • Plain text body: POST the output itself (for example --data-binary @report.txt). Size and keyword checks run on it.

A cron or shell job that exports a CSV and reports how many data rows it wrote:

bash
#!/usr/bin/env bash
set -o pipefail
PING="https://diditrun.app/p/<your-token>"
curl -fsS -m 10 --retry 3 "$PING/start" > /dev/null

if /opt/jobs/export.sh > /tmp/export.csv; then
  rows=$(tail -n +2 /tmp/export.csv | wc -l | tr -d ' ')   # data rows, header skipped
  curl -fsS -m 10 --retry 3 "$PING?items=$rows" > /dev/null
else
  curl -fsS -m 10 --retry 3 -X POST --data-binary "export.sh exited with an error" "$PING/fail" > /dev/null
fi

A job that writes a JSON array of results:

bash
count=$(jq 'length' /tmp/results.json)
curl -fsS -m 10 --retry 3 -X POST -H 'Content-Type: application/json' \
  -d "{\"items\": $count}" "$PING"

In no-code tools, put the count or output in the body of the final HTTP step: HTTP Request in n8n, Webhooks by Zapier → POST in Zapier (a Premium app, so it needs a paid Zapier plan), HTTP → Make a request in Make. See the n8n, Zapier and Make recipes.

Step 3: set the checks on the watch

Open the watch and fill in any of the optional Sanity checks:

  • Min item count: e.g. 1. If a check-in reports fewer items, or reports no count at all, the run is flagged empty.
  • Min output size (bytes): measured on the whole request body (or the ?text= value on a GET).
  • Required keyword: must appear in the output text (not case-sensitive).

Leave a field blank to skip it.

Step 4: test it once

Send a deliberately empty check-in from your terminal:

bash
curl -fsS "https://diditrun.app/p/<your-token>?items=0"

With only Min item count set (to 1), the reply is OK (flagged empty: Reported 0 items; expected at least 1.) and you get an empty alert. The next normal run sends a recovery notice. You get one alert when a watch goes empty, not one per run.

Missed, failed, empty: what each alert means

  • Missed: no check-in arrived by the expected time plus your grace window. The job didn't run, or couldn't reach the ping URL.
  • Failed: the job called /fail.
  • Empty: a check-in arrived, but it failed one of your sanity checks.
  • Late (optional): the expected time passed and you're still inside the grace window.

On Solo and Pro, the daily roll-call email lists every watch that ran, is missing, or came back empty or failed.

What empty checks can't do

They catch "nothing" and "too little". They can't tell whether non-empty output is correct. If you need that, add your own validation step to the job and have it call /fail with a short reason when the result looks wrong. Did It Run? never runs your code or connects to your accounts; it only judges what your job sends it.

FAQ

Is empty-output detection on the free plan?

Yes. Missed, late, failed and empty detection is included on every plan, including Free (3 watches, email alerts).

My job legitimately finds nothing on some days. Won't that alert?

Only if you set a minimum it can't meet. Leave Min item count blank for that job and use a required keyword instead: have the script always print a marker such as RUN-OK when it finishes properly, and send that output. The item count still shows in the watch history if you send it.

Should I POST the whole output file?

For small outputs, yes; it makes size and keyword checks possible. For large files, send a count instead. Up to 100,000 characters of a request body are read, and the first 500 characters of the text are kept in the watch history.

Can I check in by email instead of a URL?

Not yet. Email check-ins are coming soon. Today every check-in is an HTTP request to the watch's ping URL, with /start and /fail variants.

Will I get an alert on every empty run?

No. You get one empty alert when the watch turns empty, and a recovery notice when a run passes the checks again.

Know the next time it doesn't run

Know when your AI agents and automations didn't run. Free for 3 watches.

Start free