# One heartbeat, not 84 cron jobs: the scheduler at the core of a self-hosted agent

> Source: <https://dev.to/openamer/one-heartbeat-not-84-cron-jobs-the-scheduler-at-the-core-of-a-self-hosted-agent-2i6l>
> Published: 2026-10-10 22:07:21+00:00

*Why an autonomous desktop agent should own a clock instead of outsourcing it to the operating system's job runner — and what that buys you when things fail.*

If you run an agent that is supposed to keep working while you sleep, you eventually face a scheduling problem. The obvious answer is the system's own job runner: cron on Linux, Task Scheduler on Windows. Start with five jobs. Then twelve. Then you are at eighty-four, each with its own interval, its own log, its own way of failing silently — and no single place where you can answer the only question that matters at 3am: *what is actually running, and what is stuck?*

OpenAmer is an open-source desktop AI agent (Apache-2.0) that runs locally on Windows. This post is the deep-dive I get asked for most: the **ASI heartbeat** — a single in-process loop that replaced our pile of scheduled jobs.

Job-per-concern looks clean in a diagram and rots in practice, for three reasons:

The deeper issue is architectural: **the schedule lives outside the agent.** The agent's own logic cannot see, reason about, or adjust its own cadence. That is backwards for a system whose whole job is to keep itself running.

The heartbeat is a single loop that owns cadence for the whole system. Each **subsystem** declares an interval; the loop checks whether it is *due* and, if so, runs it — in the same process, via a direct function call.

```
# tools/asi/heartbeat.py  (conceptually)
class Heartbeat:
    SUBSYSTEMS = {
        "learning": 300,   # every 5 min
        "system":   300,   # self-heal, resource monitor
        "senses":   1800,  # circadian, trend scout
        "swarm":    1800,
        "infra":    1800,  # env check, browser, plugin, mesh
        "meta":     3600,  # reflection, goal, research
        "security": 14400, # bugbot, CVE scan, code review
        "a2a":      14400, # brain export, peer comms
        "darwin":   900,   # evolution, autopatch, publish, probe
        "outreach": 10800,
    }

    def tick(self, system=None, force=False):
        for name, interval in self.SUBSYSTEMS.items():
            if system and name != system:
                continue
            if force or self.is_due(name, interval):
                self.run(name)          # direct call, not a subprocess
                self.mark_ran(name)     # persisted timestamp
```

Last-run timestamps live in a small JSON state file (`memory/asi_heartbeat.json`), so the cadence survives across process restarts. The loop is driven by **one** scheduler entry — `asi-heartbeat-tick`, every five minutes — and every subsystem rides on top of it.

The subsystems are not arbitrary; they are the organs the agent needs to stay alive and improve:

| Subsystem | Cadence | What it covers | 
|---|---|---|
| `learning` | 5m | internet learner, active learn, knowledge transfer | 
| `system` | 5m | self-healer, resource monitor, traffic cop | 
| `darwin` | 15m | evolution, autopatch, publish, probe | 
| `senses` | 30m | circadian rhythm, watchtower, trend scout | 
| `swarm` | 30m | swarm intelligence, autonomous loop | 
| `infra` | 30m | env check, browser, plugin, mesh, cache | 
| `meta` | 60m | reflection, self-rewriter, goal, research | 
| `security` | 240m | bugbot, CVE scan, pen test, code review | 
| `a2a` | 240m | brain export, peer communication | 
| `outreach` | 180m | social, GitHub, growth report, funding | 

Cadence lives in one dict. Changing "how often does the agent learn from the internet" is a one-line edit, not a hunt through a job runner's UI.

The heartbeat is only half the story. The other half is *what a job runs*.

Previously each capability was an external script invoked as a process:

``` python
# BEFORE: a process per capability
import subprocess
subprocess.run([sys.executable, "scripts/training/self_model.py"])
```

Now each subsystem is imported and called directly:

``` python
# AFTER: a function call
from tools.asi import self_model
state = self_model.gather_state()
```

Three things fall out of this:

The same five capabilities are also exposed as native agent tools — `asi_status`, `asi_think`, `asi_learn`, `asi_remember`, `asi_trigger` — and as a CLI:

```
openamer asi status      # full system + heartbeat health
openamer asi heartbeat   # tick the loop (optionally one subsystem)
openamer asi trigger <capability>
```

This is not free lunch, and the trade is deliberate:

The point is not that a heartbeat is clever. It is that **an autonomous system should own its own clock, its own state and its own health — in one place it can measure.** Once scheduling is a function call inside the agent, "is the agent healthy?" stops being an archaeology exercise across a job runner and becomes a single status query.

Code and the heartbeat module live here: **[https://github.com/openamer/openamer](https://github.com/openamer/openamer)** — the scheduler is under `tools/asi/heartbeat.py`.

If you've run a long-lived agent in production: what finally made you move scheduling *into* the system, or what made you keep it outside? I'm especially curious about the failure-isolation patterns people land on.
