I’ve been running an AI personal assistant (via OpenClaw) for a couple of months now. It checks my emails, sends me a daily news digest, prepares GTD rituals, tracks weather before my morning runs, and does a dozen other things automatically.

Last week I noticed something off: I hadn’t received a daily email notification in a while. Not an error. Not a crash. Just… silence. The job was running. The logs looked fine. But nothing was arriving.

That’s the thing about silent failures — they’re the hardest to catch.


The Non-Determinism Problem

Here’s what makes AI automation fundamentally different from traditional cron jobs:

A bash script does the same thing every time. An AI agent doesn’t. Each run involves a language model making decisions — about what to format, what to include, how to route a message, whether something is “important enough” to send. Even without a code change, model updates, context drift, or slight variations in input can push the agent down a completely different reasoning path.

And when you’ve tuned a system carefully — configured scoring thresholds, routing rules, delivery modes — that non-determinism can undo months of work in a single run. Silently.


What Actually Went Wrong (Real Examples)

Over the past few weeks, I hit several of these silent failures. Here’s a tour:

1. Double messages from delivery: announce

Early on, my cron jobs used delivery: announce — OpenClaw’s built-in mode that auto-posts the agent’s output after each run. But the agent was also calling the message tool explicitly to route to the right Telegram topic. Result: every notification arrived twice.

The fix was simple once I understood it: delivery: none + explicit message tool call. The agent takes full responsibility for routing. No automatic delivery, no double-posting. All 18 active jobs now follow this pattern.

Lesson: Don’t let the framework and the agent both try to send messages. Pick one and be explicit about it.

2. Jobs routing to the wrong chat

OpenClaw has a concept of a “last active session.” Before I added explicit routing to all jobs, some notifications were landing in my DM thread instead of the right Telegram group topic — because that happened to be the most recently active session when the job fired.

Fixed by adding explicit delivery.to to every job, with a hardcoded group ID and topic ID. No ambiguity.

Lesson: Never rely on implicit defaults for automated messages. Hard-code the destination.

3. The email system fetching the wrong emails

The original email processor queried Gmail for is:unread label:important — capped at 30 results. This sounds reasonable. But Gmail’s “important” label is its own ML system, and it disagreed with my filtering logic. High-priority emails were being flagged as unimportant by Gmail and skipped entirely.

The fix: switch to after:YYYY/MM/DD with a 1000-item limit, filter by already-processed IDs to deduplicate. Our scoring engine — not Gmail’s — decides what matters.

Lesson: Don’t outsource filtering to a black box upstream. Pull everything, decide yourself.

4. Bank emails silently blocked

A legacy filter rule was too broad. It matched my bank’s marketing emails — but also caught security alerts and transaction confirmations. Everything from my bank was being dropped silently.

You don’t notice this until you’re waiting for a payment confirmation that never arrives.

Lesson: Overly broad ignore rules are a liability. Be specific. Test with real samples.

5. The shell wrapper that ate errors

Early cron jobs ran a shell script that called a Python processor and parsed stdout. If the Python script crashed, the shell wrapper returned exit code 0 anyway. Cron saw success. The agent saw nothing to report. Silence.

Refactored to call the Python script directly, have it return structured JSON, and made the agent responsible for interpreting and reporting. A crash now surfaces as a job failure, not a quiet no-op.


The Pattern: Build Self-Control In

After debugging these failures, I’ve converged on a set of principles for reliable AI-driven automation:

Explicit over implicit

Hard-code destinations, delivery modes, and routing. Don’t rely on defaults or “last active session” inference. Write it down in the job config.

Structured output over text parsing

If your cron job calls a script, have the script return JSON. Have the agent parse it explicitly. Text-in, text-out chains break silently; structured contracts surface errors.

Test suites for automation

For my email system, I added both unit and integration tests after the refactor. Not because I’m a TDD zealot — because I needed a way to verify that a model update hadn’t broken my scoring logic without running against live data.

Idempotency and deduplication

Track what you’ve processed. Use persistent state files. If a job runs twice (due to a restart, a retry, or overlapping schedules), it should produce the same result — not duplicate notifications.

Logs you can actually find

OpenClaw’s logs are timestamped by the gateway start date, not the current date. Early on I was looking for today’s logs and couldn’t find them because the gateway had been running since two weeks prior. Know where your logs live and how they’re named.

A health check built into every ritual

This is the most important one.

I embedded a Cron Health Sanity Check at the end of my daily and weekly GTD rituals. At the end of each run, the agent calls OpenClaw’s cron API, fetches the full job list, and checks every enabled job against three failure modes:

Issue 1 — Overdue: Is the job overdue by more than one interval? Different jobs have different thresholds:

  • Hourly email check: flag if more than 2 hours since last run (during daytime hours)
  • Daily jobs (news digest, disk space, backups): flag if more than 26 hours
  • Every-3-day digest: flag if more than 80 hours
  • Weekly jobs: flag if more than 8 days
  • Monthly jobs: flag if more than 35 days

Issue 2 — No-op: Did the job run but do nothing? A lastDurationMs < 2500 on a job that runs a script is suspicious — it likely returned before executing anything meaningful.

Issue 3 — Errors: Is consecutiveErrors > 0? That’s a direct failure signal.

The output at the end of the ritual is either:

🔧 Cron Health
✅ All systems healthy

or a list of flagged jobs with the issue type. This runs every evening (and again on Sundays as part of the weekly review).

The key insight: I’m not running a separate monitoring job — I’m attaching the health check to a ritual I already review every day. It costs nothing extra, and it means silent failures surface within 24 hours.


The Bigger Point

Traditional automation fails loud. A missing binary, a bad exit code, a network timeout — these announce themselves. You get an error notification, a failed job status, something.

AI automation fails quiet. The agent runs. It produces output. It just doesn’t do what you intended. And because you’ve seen it work before, you assume it’s still working.

The solution isn’t to trust the agent less. It’s to build verification into the system: health checks embedded in daily rituals, structured contracts between components, idempotency, explicit routing, and test coverage for the logic that matters.

Don’t assume your automated systems are running. Check that they are.

Related: The Self-Optimisation Loop — teaching the AI to review its own workspace and catch drift before you do. And Your AI is Only as Smart as Its Brain — on why model quality is the first line of defence against silent failures.