One heartbeat, not 84 cron jobs: the scheduler at the core of a self-hosted agent A developer behind OpenAmer, an open-source Apache-2.0 desktop AI agent that runs locally on Windows, replaced 84 separate cron and Task Scheduler jobs with a single in-process "ASI heartbeat" loop that owns scheduling for the whole agent. Each subsystem declares an interval in one dictionary, last-run timestamps persist to a JSON state file, and capabilities are invoked as direct function calls rather than subprocesses, so cadence survives restarts and can be changed with a one-line edit. 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