Most AI agent frameworks treat the agent as a long-lived thing you talk to. Boot up LangChain. Open a chat. Send a message. Wait for the loop.
That's fine for demos. In production it's the wrong shape. The useful agentic work - watching, triaging, summarizing, reporting - is not interactive. It's scheduled. And once you adopt that framing, half your reliability problems vanish.
This post is the pattern I ship: cron-driven agents with a [SILENT] return convention, wakeAgent gating, and a shared wiki for memory between runs.
The shape
Two profiles running on one host:
- yui - infrastructure-focused (PR review, health checks, backlog, security)
- anna - information-focused (news curation, research, conversation)
Each profile has its own .env (different API keys, different Matrix rooms, different skill sets). Each runs as a systemd user service. Crontabs live in jobs.json:
{
"jobs": [
{
"name": "pr-review",
"schedule": "0 */2 * * *",
"prompt_file": "prompts/pr-review.md",
"inactivity_timeout": 600
},
{
"name": "health-check",
"schedule": "*/10 * * * *",
"prompt_file": "prompts/health-check.md",
"wakeAgent": { "script": "scripts/anything-unhealthy.sh" }
},
{
"name": "backlog-worker",
"schedule": "0 */2 * * *",
"prompt_file": "prompts/backlog.md",
"inactivity_timeout": 1800
},
{
"name": "daily-summary",
"schedule": "0 8 * * *",
"prompt_file": "prompts/daily.md"
}
]
} No web server. No chat UI. A scheduler and a prompt file per job.
The [SILENT] return convention
Naive cron-driven agents spam you. Every tick: 'I checked the PRs and here's what I found. (Nothing to do.)' Do that every two hours for a week and you stop reading the messages.
The convention: the agent returns [SILENT] when there is nothing to report. The delivery layer treats [SILENT] as suppress-this-delivery. No Matrix message goes out. No email fires. The agent might have done real work - reviewed twelve PRs, decided none need changes, updated three wiki entries - but if the outcome is 'all clear,' the user doesn't hear about it.
In the prompt:
# PR Review - yui
Run the pr-review skill. Merge or comment on any clean PRs.
On completion, emit one of:
1. A single-line Matrix summary if at least one PR was actioned
(merged, commented, closed).
2. [SILENT] if no PR required action this cycle.
Do not explain what you didn't do.
Do not report 'nothing found.'
Do not include a process summary.
One line, or [SILENT]. In the delivery code:
response = agent.run_conversation(prompt)
if response.strip().startswith('[SILENT]'):
logger.info('cron job produced silent outcome, not delivering')
return
matrix.send(room=MAIN_ROOM, body=response) This one convention turned my agent channels from noisy into readable.
wakeAgent - skip the LLM entirely
Most scheduled agent work has a precondition. Health checks only matter if something's unhealthy. News summaries only matter if news broke. Backlog workers only matter if there's a backlog.
wakeAgent is a config key that points at a shell script. The script runs before the agent. If it exits 0, the agent runs. If it exits non-zero, the whole cycle skips and no LLM call happens.
#!/bin/bash
# scripts/anything-unhealthy.sh
# Exits 0 if any container is unhealthy, 1 otherwise.
if podman ps --format '{{.Status}}' | grep -qE 'unhealthy|Restarting'; then
exit 0
fi
if systemctl --user list-units --state=failed | grep -q "failed"; then
exit 0
fi
exit 1 Most cycles, nothing is unhealthy, the script exits 1, the agent doesn't run. LLM spend drops to near-zero on a stable infrastructure. When something is wrong, the agent wakes up, diagnoses, and pings.
This is the cron-agent equivalent of an embedding gate: spend the cheap compute first, only invoke the expensive part when it's actually needed.
Shared wiki as memory
Stateless agent runs lose context between invocations. You can't just put everything in the system prompt - it's too much, and it contaminates the agent's attention.
The pattern I adopted from Andrej Karpathy's LLM wiki thinking: a shared, append-mostly knowledge store the agent queries before acting and writes to after learning. I use OpenViking (a Rust-based knowledge store with an MCP-compatible HTTP API) but any document-db-with-search works.
viking://resources/
├── wiki/ # reference knowledge
│ ├── infrastructure/hermes.md
│ ├── operational-patterns.md
│ └── troubleshooting.md
├── shared/ # runtime state between agents
│ └── backlogs/yui.md
└── agents/ # per-agent scratch Hard rules for the agents:
- Query the wiki before acting on an assumption. 'Do I already know how to handle this error?'
- Write back to the wiki after learning. 'What's the one-line fix I just figured out?'
- Wiki stores only knowledge content - never status, never summaries, never 'current state.' Those belong in Matrix messages.
- Deletions are cheap. An agent that can't prune noise will suffocate in it.
LLM serialization (separate post, mentioned here)
If you run a single-slot local inference server (llama.cpp --parallel 1), concurrent cron jobs + chat can starve each other at the HTTP queue. I handle this with a file-lock in gateway/llm_lock.py; the cron job skips gracefully (returns [SILENT]) if the lock is held. Detailed treatment in a follow-up post .
Why this beats persistent chat loops
Three reasons:
1. Reliability. A systemd user service running cron is a trivially-debuggable pattern every ops person knows. A persistent chat loop with a stateful WebSocket connection has ten more ways to die quietly.
2. Cost. [SILENT] + wakeAgent means agents don't burn tokens when there's nothing to do. On my setup this was a 5-10× reduction vs. a polling-inside-a-loop model.
3. Composition. Cron agents compose naturally with other scheduled infra. They are scheduled infra. They belong next to your backup crons, your TLS cert renewals, your log rotation.
The chat-loop framing assumes the agent is the system's center of gravity. Cron framing assumes the agent is one more background worker alongside everything else. I've come around hard to the second framing.
What I'd build next
If you're wiring this yourself, the sequence I'd follow:
1. Start with one cron job, one agent, one Matrix channel. Get [SILENT] working. Resist the urge to add anything else for a week.
2. Add wakeAgent to the cheapest job you have.
3. Stand up a shared wiki. Write one reference article together with the agent.
4. Only then add a second cron job. The temptation to 'schedule everything' before getting one job reliable is the single biggest trap.
No comments yet