How to know if your AI agent stopped running

Scheduled agents rarely crash loudly. They just stop: a key expires, a laptop sleeps through 6 a.m., a run "succeeds" with an empty answer. Here's how to find out what happened right now, and how to make sure you hear about it within minutes next time.

Updated October 30, 2026 · 9-minute read

Short answer: don't wait for an error, because there often isn't one. Have every run check in with an outside monitor when it finishes, along with a small summary of what it produced. If the check-in doesn't arrive on schedule, or the summary looks wrong, you get alerted. This is called a heartbeat, or a dead man's switch.

Why AI agents fail without telling you

A scheduled agent has more ways to go quiet than an ordinary cron job, and most of them produce no error message anyone will see. They fall into four groups:

What happenedTypical causesDoes anything alert you?
It never startedThe machine was asleep, rebooted or out of disk. The scheduler was paused, the workflow was deactivated, or GitHub disabled a scheduled workflow after 60 days without repository activity (public repos).No. There's no run, so there's no failed run.
It started and diedExpired or rotated API key, revoked OAuth token, 429 rate limits, a hit spending cap, a retired model name, an out-of-memory kill.Only if you're watching logs, and the agent didn't swallow the error.
It hungAn API call with no timeout, a tool waiting for input, a loop that never reaches its stop condition. Later runs can pile up behind it.No. The process is still "running".
It "succeeded" with nothing to showA search returned zero results, the model refused or apologized, a tool error was caught and paraphrased, a file was written empty.No. Exit code 0 and a green check.

The last group is the one that bites hardest with agents. An agent loop is designed to keep going when a tool fails, so "the process finished" tells you very little about whether the work got done.

Check whether it ran, right now

If you suspect a run was missed, work from the outside in: first confirm the trigger fired, then look at what the agent did.

  1. Look for the output, not the logs. Is today's report in the folder, the row in the sheet, the message in Slack? If the deliverable is missing, you already know the answer.
  2. Check the scheduler. Did cron, GitHub Actions, n8n or your agent platform start a run at the expected time? If no run exists, the problem is the trigger or the machine, not the agent.
  3. Read the run's result. If a run exists, look at its exit code and, for agents, the structured result. Claude Code's --output-format json includes subtype and is_error; anything other than a clean success is worth reading.
  4. Check the provider. Rate limits, spending caps and key problems show up in your model provider's usage dashboard before they show up anywhere else.
# Plain cron: did the scheduler even start the job?
grep CRON /var/log/syslog | tail -20        # Debian/Ubuntu
journalctl -u cron --since "24 hours ago"   # systemd hosts (the unit may be "crond")

# GitHub Actions: list recent scheduled runs of one workflow
gh run list --workflow nightly-agent.yml --event schedule --limit 10

# Claude Code: run the task by hand and read the result fields
claude -p "your task" --output-format json | jq '{subtype, is_error, num_turns}'

This works once. The point of the next section is that you shouldn't have to do it again.

Set up a heartbeat so you're alerted next time

A heartbeat flips the question around. Instead of searching for a failure, you wait for a success, and get told when it doesn't come. Because it watches for silence, it catches every failure in the table above, whatever the cause.

1. Write down the schedule

Be specific: "every weekday by 06:30", "every hour", "at least once a day". Then pick a grace period that covers normal variation: the longest normal run plus any scheduler delay. A daily agent that takes up to 15 minutes might get 30 minutes of grace. GitHub Actions schedules can start well after the scheduled time under load, so allow 30 to 60 minutes there.

2. Create a monitor and copy its ping URL

Use a monitor that runs somewhere else, not on the same machine or n8n instance as the agent, because a check that lives with the job dies with it. In Pulsecheck, add a job with your period and grace and you get a unique ping URL like https://pulsecheck.run/p/YOUR-JOB-ID.

3. Ping at the end of every successful run

Make the ping the last step, after you've checked the output. A ping at the start only proves the job began. A ping at the end, behind a check, proves the work got done. Any GET or POST to the URL counts. Use a short timeout and a retry, and never let a failed ping fail an otherwise good run.

4. Report failures straight away

From your error path, call the same URL with /fail on the end. You hear about it immediately rather than when the deadline passes, and anything you POST with it (an error message, an exit code) shows up with the alert.

5. Send a summary and add output rules

This is what turns "it ran" into "it worked". POST a tiny JSON summary of the run, such as {"count": 12, "report_url": "https://..."}, then add rules like count at least 1 or must include report_url. A run that breaks a rule is flagged as Ran but output looks wrong, even though it exited cleanly. (Output checks are on Pulsecheck's Pro and Agency plans. Missed-run and failure alerts work on every plan, including the free one.)

6. Test the alert

Run the job once by hand and watch the monitor go green. Then break it on purpose: point it at a bad API key, or simply skip a run, and confirm the alert actually reaches your inbox or Slack channel. An alert you've never seen fire is an alert you can't trust.

Copy-paste examples

Replace YOUR-JOB-ID with the ID from your own ping URL.

Claude Code (or any headless agent CLI)

Run the task headlessly, read the structured result, then report what was produced. The timeout makes sure a stuck run ends and gets reported instead of hanging silently.

#!/usr/bin/env bash
# run-digest.sh: run a Claude Code task headlessly and report the result to Pulsecheck
set -uo pipefail
PING="https://pulsecheck.run/p/YOUR-JOB-ID"

# Hard time limit so a stuck run can't hang forever
out=$(timeout 30m claude -p "Write today's client digest to out/digest.md" \
  --output-format json --allowedTools "Read,Write")
code=$?

# Did the agent itself report an error?
is_error=$(echo "$out" | jq -r '.is_error' 2>/dev/null || echo unknown)
subtype=$(echo "$out" | jq -r '.subtype // "none"' 2>/dev/null || echo none)

if [ "$code" -ne 0 ] || [ "$is_error" != "false" ]; then
  curl -fsS -m 10 -X POST "$PING/fail" -H "Content-Type: application/json" \
    -d "{\"exit\": $code, \"subtype\": \"$subtype\"}"
  exit 1
fi

# Report what the run actually produced, not just that it ended
lines=$( [ -s out/digest.md ] && wc -l < out/digest.md || echo 0 )
curl -fsS -m 10 --retry 3 -X POST "$PING" -H "Content-Type: application/json" \
  -d "{\"count\": $lines, \"report\": \"out/digest.md\"}"

Schedule it with cron, launchd or your agent platform, and set an output rule of count at least 1 so an empty digest raises an alert.

Python agent

import requests

PING = "https://pulsecheck.run/p/YOUR-JOB-ID"

def main():
    try:
        leads = run_agent()          # your agent: returns the items it produced
    except Exception as e:
        # Tell Pulsecheck straight away instead of waiting for the deadline
        requests.post(f"{PING}/fail", json={"error": str(e)[:300]}, timeout=10)
        raise

    # Send a summary. An output rule like "count at least 1" catches empty runs.
    try:
        requests.post(PING, json={"count": len(leads), "source": "crm"}, timeout=10)
    except requests.RequestException:
        pass  # never let the monitoring call break a good run

if __name__ == "__main__":
    main()

Cron on a server

flock -n stops runs piling up behind a stuck one, and timeout bounds each run. The success ping only fires if the job exits 0; anything else calls /fail.

# m  h  dom mon dow  command
# Every day at 06:00: no overlapping runs, a 30-minute limit, success or /fail ping
0 6 * * * if flock -n /tmp/agent.lock timeout 30m /opt/agent/run.sh; then curl -fsS -m 10 --retry 3 https://pulsecheck.run/p/YOUR-JOB-ID; else curl -fsS -m 10 https://pulsecheck.run/p/YOUR-JOB-ID/fail; fi

GitHub Actions schedule

Keep the ping URL in a repository secret. Remember that the cron in a workflow runs in UTC, and give the monitor generous grace for late starts.

# .github/workflows/nightly-agent.yml
name: Nightly agent
on:
  schedule:
    - cron: "0 14 * * *"     # GitHub schedules run in UTC
  workflow_dispatch: {}
jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - run: python agent.py > result.json   # prints a JSON summary, e.g. {"count": 12}
      - name: Ping Pulsecheck
        if: success()
        run: |
          curl -fsS -m 10 --retry 3 -X POST "${{ secrets.PULSECHECK_URL }}" \
            -H "Content-Type: application/json" --data @result.json
      - name: Report failure
        if: failure()
        run: curl -fsS -m 10 "${{ secrets.PULSECHECK_URL }}/fail"

n8n

n8n's Error Workflow only fires when an execution starts and then fails. If the trigger never fires, or the workflow is switched off, there's no execution and no error, which is why the heartbeat matters.

Last node of the workflow: HTTP Request
  Method: POST
  URL: https://pulsecheck.run/p/YOUR-JOB-ID
  Send Body: on, Body Content Type: JSON
  JSON: {"count": {{ $items().length }} }
  Settings: Execute Once = on, On Error = Continue

Error path: a separate workflow that starts with an Error Trigger node,
followed by an HTTP Request node that POSTs to https://pulsecheck.run/p/YOUR-JOB-ID/fail.
Then pick that workflow under Workflow Settings → Error Workflow.

Common mistakes

FAQ

What is a heartbeat monitor?

A heartbeat monitor (also called a dead man's switch) waits for your job to check in. Each run sends a request to a unique URL when it finishes. If the request doesn't arrive within the expected interval plus a grace period, the monitor alerts you. It catches failures that leave no error behind, such as a scheduler that stopped firing or a machine that was asleep.

How long should the grace period be?

Use the longest normal run time plus any scheduler delay. For a daily agent that takes up to 15 minutes, 30 minutes of grace is reasonable. GitHub Actions schedules can start late under load, so allow 30 to 60 minutes there.

Can I monitor an agent that runs on my laptop?

Yes. The monitor lives outside your machine, so if the laptop is asleep, offline or shut down at run time, the ping never arrives and you get alerted. That is exactly the failure an on-machine check can't report.

How do I catch an agent that runs but produces nothing useful?

Send a small summary with the ping, such as {"count": 12}, and set an output rule like "count at least 1" or "must include field report_url". In Pulsecheck, output checks are on the Pro and Agency plans. A run that breaks a rule is flagged as "Ran but output looks wrong" even though it technically succeeded.

Should the ping go at the start or the end of the run?

At the end, after you've checked the output. A ping at the start only proves the job started, so it misses runs that crash, hang or finish empty.

What should I send in the payload?

Counts, flags and field names, such as how many items were processed or whether a file was written. Avoid customer data or secrets; the ping is a status signal, not a log.

Hear about it before your clients do.

Pulsecheck gives every agent, workflow and cron job its own ping URL, and alerts you by email or Slack when one goes quiet, fails, or comes back with nothing. Free for 3 jobs.

Start free

More guides: all Pulsecheck guides.