Your agent reported success.
It produced nothing.

Scheduled agents fail quietly. A run that never fired, a loop that swallowed a tool error and exited 0, a model call that never returned. SteadyCron watches the outcome instead of the exit code, and tells you when a real result stops arriving.

No card. One agent monitor free, with every rule switched on.

What the run looked like

02:00  scheduled run started
02:04  process exited 0
02:04  ✓ marked successful

# out/digest.md → 0 bytes
# tokens spent: 84,300
# anyone alerted? no

What you actually needed to know

02:00  /start          run began
02:04  report: items=0
02:04  rule: empty_result → failed

→ #ops-slack  nightly-digest failed
  last good run: yesterday 02:03

For an unattended agent, silence looks like success

Agent frameworks are built to keep going. A tool call fails, the model apologises and moves on, the loop finishes cleanly with nothing written. The process exits 0 because the process ended — not because work happened.

Running unattended was the whole point, so nobody is reading the output. The scheduler will not report a dead schedule, the provider will not report an exhausted spend cap, and the agent will not report its own empty run.

The distinction that matters

An agent reporting all-clear with zero throughput isn't healthy — it's asymptomatic.
The failure mode this page exists for
  • Exit code 0 with no artifact — the most common agent failure, and the one every uptime check passes.
  • The run never fired at all: a disabled workflow, a sleeping machine, a dropped schedule event.
  • A model call without a timeout wedges the run for hours, and the next runs pile up behind it.
  • Rate limits, an exhausted budget, or a rotated API key end tonight's run early — and tomorrow's too.
  • A deprecated model ID or a reshaped response breaks parsing on the provider's timeline, not your deploy timeline.

Caught by a schedule expectation, not a stack trace

Tracing tools record what a run did. None of them can record a run that never happened — there is no trace to emit. That takes something watching from the outside, on a clock.

nightly-digest NEVER RAN

The run never happened

GitHub Actions disabled the workflow after 60 days of repo inactivity. Nobody noticed for three days, because a run that does not start produces no log, no error, and no trace.

expected at 02:00 daily
last success 3 days ago
overdue by 72 h

Only an external schedule expectation catches an absence. This is the failure your observability stack structurally cannot see.

ticket-triage EMPTY

It ran, and produced nothing

The search tool failed, the model apologised, the loop completed. Four minutes of tokens spent and zero rows written — reported as a success, because the process ended without throwing.

completed 02:04
duration 3 m 51 s
rows written 0

Your run reports the count; the empty-result rule turns a zero into a failure and an alert. You do not have to remember to implement the check.

embedding-refresh STUCK

It hung on a model call

Many SDKs ship without a request timeout. One wedged connection and the run never finishes. The /start ping arrived; /success never did.

start received 09:00:12
agent run time 30 min
success received never

The start ping arrived on time, so this is not a missed schedule — it is a hung run, and we report it as one. Your trigger is fine; the agent is not.

Five lines, wherever your agent runs

Ping when the run starts, and ping success only after checking that the output exists. That second half is the whole trick — it is what turns an empty run into an alert.

# Anthropic runs the schedule — but a routine that gets skipped, or
# finishes with nothing, tells nobody. Connect SteadyCron's MCP server
# and the agent reports its own runs with a tool call.

MCP server:  https://api.steadycron.com/mcp
Auth:        Bearer <your SteadyCron API key>

# Then, in the routine's prompt:

Call report_run_start with jobKey "nightly-digest" before you begin.
This is required — report_run is refused if no run is open.

Do the work. Write the result to out/digest.md.

When you are done, count what you actually produced and call report_run
with that count. If you produced nothing, report 0 — do not omit the
field, and do not round up.

    report_run { jobKey: "nightly-digest", itemsProduced: 42,
                 model: "claude-opus-5", tokensIn: 18400, tokensOut: 2100 }

# SteadyCron turns a 0 into a failed run and an alert. No curl to get
# wrong, and a tool call is far harder for the model to skip than a
# sentence in the prompt.

Full walkthrough for each platform, plus the gotchas, in Monitoring scheduled AI agents.

One small JSON body, and what each field switches on

The recipes above post a handful of numbers when the run finishes. Every field is optional — a partly instrumented agent is still monitored — but each one turns something on, and nothing here asks you to compute a verdict yourself.

What you can send

itemsProduced
How many things the run actually made. Zero fails the run. This is the field the whole monitor kind exists for.
model + tokensIn / tokensOut
We price the run from a built-in model price table, so per-run cost arrives without you working it out.
costUsd
Already know the figure? Send it and we use yours instead of deriving one.
steps / toolCalls
Ceilings that catch a loop — the same tool called forty times while the run burns tokens without finishing.
traceUrl
The alert links straight to that run in Langfuse, LangSmith, or wherever you record spans.
summary
Attached to the alert your on-call actually reads. Send the raw error, not a paraphrase.

Spend you see before the invoice does

Every priced run rolls into a month-to-date total per monitor, and you can set a ceiling per run or across the month. A model we have no price for is recorded as unpriced rather than free: a total that quietly under-reports is worse than no total at all.

Leaving a field out is fine — a rule with nothing to read simply does not evaluate. Whatever you do send is kept verbatim alongside the parsed numbers, so when a count looks wrong you can see exactly what your agent reported.

A narrow question, answered reliably

SteadyCron is not an LLM tracing tool and does not pretend to be one. It watches the run, not the reasoning.

What this does

  • Alerts when a scheduled run never happened — the one failure a trace cannot record.
  • Alerts when a run finishes having produced nothing. We check the number your run reports, so this is not something you have to remember to implement.
  • Alerts when a run costs more than your ceiling — per run, or across the month.
  • Alerts when a run's step or tool-call count says it is looping.
  • Alerts when a run starts and never finishes, against a run time you set.
  • Tells a run that never started apart from one that started and hung — two clocks, two incidents, two different fixes.
  • Prices each run from the tokens and model it reports, and totals the month per monitor.
  • Flags a run that claimed success without sending the report it owes you, instead of taking the claim at face value.
  • Keeps the exact JSON every run sent, for the moment a number looks wrong.
  • Shapes the noise — grace periods, consecutive-failure thresholds, quiet hours, auto-resolve.

What it does not do

  • Span-level traces of every model call and tool invocation.
  • Prompt management, datasets, or LLM-as-judge evaluation.
  • Scoring answer quality, or detecting hallucinations.
  • Telling you why the model made a particular decision.

For those, use a tracing tool — Langfuse, LangSmith, and friends do it well. Put the trace URL in the ping payload and your alert links straight to the run. SteadyCron answers the questions they cannot: did it run, did it finish, did it produce anything, and what did it cost.

Agent runs are the most sensitive logs you have

A backup script logs "complete". An agent run carries prompts, retrieved documents, tool arguments, and whatever customer data passed through the context window — and you are about to send a summary of it to a monitoring vendor.

SteadyCron runs on Hetzner servers in Germany, operated by a German legal entity, under a self-serve DPA you can read and sign without a sales call. You also choose what goes in the ping: attach a row count instead of the rows.

  • Hetzner, Germany
  • German legal entity
  • Self-serve DPA
  • Public sub-processor list
EU hosting and data residency

Point a monitor at tonight's run

Create an agent monitor, set the schedule your agent is meant to keep, and add the two pings. If tomorrow's result does not arrive, you will know before anyone asks.

  • Free plan, no card
  • 1 agent monitor free
  • EU-hosted
  • Paid from €10/mo