◉ANTIGRAVITY LABJP
Articles/Agents & Manager
◈ Agents & Manager/2026-09-06Advanced

With scheduled agents, I now look for runs that never happened before I look for failures

My run ledger showed a 100% success rate while the evening slot had not fired for two weeks. Here is the reconciliation I now run against an expected-fire table, with measured notes on cron expansion, exit codes, and how the ledger itself gets written.

Antigravity392agents146scheduled runs5cron3operations33

✦ Premium Article

I had increased a wallpaper classification batch to twice a day, or so I believed, until the morning the processed counts refused to add up. The ledger looked fine. Every entry said OK. There were no errors anywhere.

The counts were still short. I read the timestamps one by one and finally saw it. Only the morning slot was there. The evening slot had not appeared once in two weeks.

Until then I had been measuring the health of my unattended work by the absence of failures. If nothing failed, I assumed things were fine. But a run that never happens is not recorded as a failure either. Nothing lands in the ledger, and nothing rings. All that remains is the fact that nothing happened.

Since then I have turned the direction of my monitoring around. I did not stop looking at what happened. I added a step before it: counting what was supposed to happen.

Failure alerts cannot reach a run that never started

Written out, the reason is obvious. A failure alert fires only once a process is alive. A run that never started has no process, and therefore nothing to raise the alert.

Antigravity's Remote Control push notifications behave the same way. They arrive when an agent completes a task, or when it needs more input from you — that is, only for runs that began. Those notifications have saved me plenty of evenings, but they are not a channel that can tell you "today it simply did not run."

Counting on my own ledger, three days of records gave me this:

matched runs        : 6
missing runs        : 3
caught by failure-only alerting: 0

The success rate is 6/6, a clean 100%. Read from the side of what was expected, the same ledger is 6/9, or 67%. Same data, different denominator, opposite conclusion.

Count the runs that should have happened, not the ones that succeeded. Putting that sentence at the top of my operations notes was the point where I started trusting my overnight work again.

Hold the expectation as a table, not as an expression

When I started digging, I suspected the cron expression first. It read 30 4,16 * * *. I could not find anything wrong with it. Validating the expression itself led nowhere.

So I stopped reading it and expanded it instead. A short function that opens up the minute and hour fields is enough.

# expect.py - expand a cron expression into the times it should fire
# Day, month and weekday are assumed to be *. For run auditing I only needed
# the minute and hour fields.
from datetime import datetime
 
def parse_field(f, lo, hi):
    """Expand commas, ranges and steps into a set of values."""
    out = set()
    for part in f.split(","):
        if part == "*":
            out |= set(range(lo, hi + 1))
            continue
        if "/" in part:                       # */6 or 0-23/6
            base, step = part.split("/")
            rng = range(lo, hi + 1) if base == "*" else range(
                int(base.split("-")[0]), int(base.split("-")[-1]) + 1)
            out |= set(v for v in rng if (v - min(rng)) % int(step) == 0)
            continue
        if "-" in part:                       # 9-17
            a, b = part.split("-")
            out |= set(range(int(a), int(b) + 1))
            continue
        out.add(int(part))
    return sorted(out)
 
def expected(expr, day):
    """Return the datetimes that should fire on this day, ascending."""
    minute_field, hour_field = expr.split()[0], expr.split()[1]
    minutes = parse_field(minute_field, 0, 59)
    hours = parse_field(hour_field, 0, 23)
    return [day.replace(hour=H, minute=M, second=0, microsecond=0)
            for H in hours for M in minutes]

Running it on my machine:

'30 4,16 * * *'      -> 2 per day  ['04:30', '16:30']
'30 4 * * *'         -> 1 per day  ['04:30']
'30 16 * * *'        -> 1 per day  ['16:30']
'0 */6 * * *'        -> 4 per day  ['00:00', '06:00', '12:00', '18:00']

As an expression, it is two firings. The syntax was never the problem. What actually ran was one. How a given runner treats a multi-slot field turned out to be a separate question from whether the expression is valid — that was the moment it finally landed for me.

Anything I want to run twice a day now lives as two separate jobs. Whatever I gain by keeping the expression compact is worth less than the risk of one half going quiet.

How it is writtenExpected by the expressionFailure modeWhat I do now
30 4,16 * * *2 per dayOne slot can fire alone, silentlyI avoid it
30 4 * * * and 30 16 * * *1 per day eachA stopped slot shows up as a gapI split it this way
0 */6 * * *4 per dayThinning is hard to noticeReconciliation is mandatory
✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You will be able to catch runs that never started at all, through a path separate from your failure alerts
✦You will be able to add expected-versus-actual reconciliation on top of the schedules you already have, without touching them
✦You will be able to measure the gap between success rate and coverage in your own setup (the same ledger reads 6/6 and 6/9)
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ Agents & Manager2026-04-26
Designing Antigravity Agent Traces That Tell You Why It Failed — Observability in Practice
How to add OpenTelemetry spans to your own agent loop so a failure can be read in a few lines. Covers span structure, what to keep and drop in attributes, a failure_class taxonomy, a retry table, three dashboard views, cost attributes, and PII handling — with code that runs.
◈ Agents & Manager2026-10-16
Before Handing an Agent a .zip: Dropping .git Cut 29 MB to 15 MB, and 28 Secret Warnings Turned Out to Be Noise
Antigravity app v2.23.0 lets you attach a .zip. I packed my own repository to count what .git weighs and how many secret-scan warnings are false alarms, and I leave the full Python script that excludes, scans, and writes a manifest.
◈ Agents & Manager2026-09-09
When an Antigravity Scheduled Task Never Runs: Check sidecar.json, Then enabled, Then the Logs
A checking order for Antigravity 2.0 Scheduled Tasks that quietly do nothing: where sidecar.json lives, how the directory name becomes the ID, the enabled flag in config.json, and the logs under sidecar_data.