OPENCLAW PLAYBOOK
CTRL+K
INITIATE_PROTOCOL
← Back to Blog

The OpenClaw Agent Message Accuracy Playbook

By Mira • October 22, 2026 • 6 min read

The credential rotation playbook mentions, almost in passing, an agent that kept adding “analytics auth is broken” to its daily summary for days after the service account had been repaired and verified. The operator corrected it three times. That agent was one of mine. The line was false every time it went out, and nothing between the agent and the operator's phone objected once.

The checks were looking at the wrong thing.

This page is about the accuracy of what agents tell people, on a host running the OpenClaw gateway with scheduled jobs that report to a human over Telegram or Slack. Who gets a message is a separate problem, and the approval gates playbook handles it. Here the recipient is right and the content is wrong.

What the Guardrail Guides Check

The pages that rank for agent output guardrails (OpenAI's Agents SDK docs, Mastra's TypeScript guide, Akto's validation guide) cover the same core. Validate the output against a schema. Screen it for PII and prompt injection. Put a tripwire in front of anything that reaches the outside world, and when a check fails, hand the error back so the agent can try again. Mastra adds a useful split between deterministic checks and ones that ask a second model to judge.

I use most of that. None of it would have caught my analytics line, because that line was well formed, contained no secrets, injected nothing and was on topic. It was simply old. A schema can tell you a message has a status field. It cannot tell you when anyone last looked.

Every Sentence Came From Somewhere

Read a scheduled summary and sort its sentences by origin. Some came from a probe or a log that ran this morning. Others came from the model's context, which on a long-lived agent means memory files plus whatever it was told last week, and those sentences read exactly as confidently as the first kind. The reader cannot tell them apart. Neither, reliably, can the agent.

A remembered problem is the worst kind, since nothing ever comes along to contradict it. A fixed service stops producing errors, and the absence of an error is not an event the agent notices. The idempotent jobs playbook describes the mirror image: a report that kept losing its resolution fields, so a morning job treated five already solved problems as new on more than seventeen consecutive runs. One agent remembered a problem that was gone. The other forgot one that was handled. Both told a human something untrue with complete composure.

Give Each Claim a Receipt

My rule is that a status claim (anything is broken, down, expired, failing or fixed) may only appear in a message if a check in the same run produced it. To make that enforceable, the agent writes its message to an outbox file instead of sending it, and lists its claims next to the body:

{
  "run_id": "digest-2026-10-22-0700",
  "body": "Morning. Two jobs ran late. Analytics probe returned 200.",
  "claims": [
    { "text": "analytics probe returned 200",
      "probe": "analytics-probe", "status": 200,
      "run_id": "digest-2026-10-22-0700" }
  ]
}

A small script sends it, and refuses when a claim carries some other run's id, or when the body uses alarm words and the claims list is empty:

#!/bin/sh
# presend: no fresh receipt, no send
MSG=$1
RUN=$(jq -r .run_id "$MSG")
stale=$(jq -r --arg r "$RUN" '.claims[] | select(.run_id != $r) | .text' "$MSG")
[ -n "$stale" ] && { echo "presend: stale claim: $stale" >&2; exit 1; }
if jq -r .body "$MSG" | grep -qiE 'broken|failing|down|expired|blind'; then
  [ "$(jq '.claims | length' "$MSG")" -eq 0 ] && {
    echo "presend: alarm word with no receipt" >&2; exit 1; }
fi

The word list is crude and will block a few innocent sentences. I accept that trade happily. A blocked summary costs a minute of my time to look at, while a false alarm repeated every morning trains the reader to skip the one line that will matter someday. Keep the script out of the agent's reach, the same way the approval gates page keeps the send credential with a separate executor, or the first agent that finds it inconvenient will route around it.

Building with OpenClaw?

Get the Starter Kit with annotated config, 5 production skills, and deployment checklist.

Grab the Starter Kit →

Recurring Messages Rot From the Template

Daily digests are where stale claims live. The easy way to write today's summary is to start from yesterday's, and an agent asked for “the usual morning brief” will often do exactly that, keeping the footer and every standing warning, then changing the parts it has new data for. Anything it has no new data for survives by default. Build each recurring message from that run's log and nothing else, and say so in the job's instructions in plain words. If an item still matters, the check that found it will find it again.

Click the Link the Way the Reader Will

Agents build URLs from patterns, and patterns go stale when hosting config changes underneath them. This site is a small example. Its vercel.json sets cleanUrls: true, so a request for a path ending in .html gets a 308 redirect to the same path without the extension. Anything that depended on the first request arriving unchanged, a one-time key handled by middleware for instance, is now depending on how the redirect treats it. The agent that wrote the link saw a correct-looking URL. The reader on a phone gets whatever the redirect chain hands them.

Before sending, fetch each link with no cookies, follow redirects, and look at where it lands:

for url in $(jq -r .body "$MSG" | grep -oE 'https://[^ )"]+'); do
  out=$(curl -s -o /dev/null -L --max-redirs 5 \
    -w '%{http_code} %{url_effective}' "$url")
  code=${out%% *}; final=${out#* }
  [ "$code" = 200 ] || { echo "presend: $url -> $out" >&2; exit 1; }
  [ "$final" = "$url" ] || echo "presend: $url redirected to $final" >&2
done

A 200 that lands on a login page still fails the reader, so for links to private pages, check that the final URL is the page you meant and not a sign-in screen. Sending the redirected URL instead of the original is usually the right fix.

Corrections Need a Home

If a person has to correct the same line three times, the correction lives in a chat window while the bug lives in a file, and only the file gets read at seven in the morning.

A Test for Tomorrow Morning

Take your most-read scheduled message and break something it reports on, harmlessly. Revoke a test key, or point a probe at a URL that returns 404. Run the job with openclaw cron run and confirm the message says so, with a status code. Then fix it and run the job again.

The second message is the real test. If the warning is still there, you have my analytics bug, and the silent failures guide has the one-line-per-run log format that makes a receipt cheap to produce.

Related Reading

Frequently Asked Questions

Why does my OpenClaw agent keep reporting a problem that is already fixed?

The warning is coming from its context or from yesterday's message, and no check in the current run contradicts it. Require every status claim to come from a probe in the same run, and build recurring summaries from that run's log instead of the previous summary.

Don't output guardrails already handle this?

Most guardrail tooling checks shape and safety: schemas, PII, prompt injection, topic. A stale claim passes all of those. Freshness needs its own check, which can be as small as matching each claim's run id to the current run.

How should an agent check links before sending them?

Fetch each one without cookies, follow redirects, and confirm the final status is 200 and the landing page is the intended one. Hosting settings such as Vercel's cleanUrls can redirect a URL the agent built from an old pattern.

Where should a correction from the operator be stored?

In a file the job reads on every run, such as its instructions or a rules file, ideally backed by a check in the presend script. A correction that only exists in chat history will not survive the next session.

Get the free OpenClaw quickstart checklist

Zero to running agent in under an hour. No fluff.