Cron and Heartbeat
Corresponding source
| Subject | Source |
|---|---|
| Cron data model | nanobot/cron/types.py |
| Scheduling, persistence, and execution | nanobot/cron/service.py::CronService |
| Agent-bound runtime boundary | nanobot/cron/bound_runner.py, nanobot/cron/session_turns.py |
| Active-Session coordination | nanobot/agent/cron_turns.py::CronTurnCoordinator |
| Gateway lifecycle integration | nanobot/cli/commands.py::_run_gateway |
Learning objectives
After this lesson, you should be able to:
- state where Cron jobs are stored;
- distinguish user reminders, Heartbeat, and Dream;
- explain how a Session-bound Cron job re-enters AgentLoop;
- explain why a Cron Turn is deferred while its Session is active.
Prerequisites
- Understand Gateway.
- Understand InboundMessage and Session Keys.
- Know basic interval and Cron schedules.
1. CronService is workspace-scoped
Gateway creates:
<workspace>/cron/jobs.jsonand passes it to CronService.
Switching workspaces produces a different set of Cron jobs by design; the tasks were not lost.
The Cron directory may also contain:
action.jsonl;runs/;- lock files.
2. Cron data model
Primary types:
CronScheduleCronPayloadCronRunRecordCronJobStateCronJobCronStore
Location:
nanobot/cron/types.py
The current source defines supported schedule forms, including:
- one-time timestamp;
- repeating interval;
- Cron expression.
3. Atomic persistence and corruption protection
CronService saves jobs.json through:
temporary file
→ flush + fsync
→ os.replace
→ directory fsync (may be skipped on Windows)If the existing store cannot be parsed:
- the corrupt file is preserved;
- a strategy such as
.corrupt-<timestamp>is used; - a parse failure is not silently treated as an empty list that later overwrites the original jobs.
This protects the invariant that “unable to parse” does not mean “no jobs exist.”
4. A user Cron job must bind to a Session
An Agent-Turn user job needs complete delivery context:
- Session Key;
- origin Channel;
- origin chat ID;
- Channel metadata.
An older job that cannot bind to a concrete Session is migrated or disabled so it never executes without knowing where to deliver its result.
5. Session-bound Cron Turn
run_bound_cron_job() converts a Job into an ordinary InboundMessage:
flowchart LR CS["CronService reaches schedule"] --> BR["run_bound_cron_job"] BR --> IM["InboundMessage<br/>with Cron metadata and Session override"] IM --> CC["CronTurnCoordinator.submit"] CC --> AL["AgentLoop"] AL --> OM["OutboundMessage"] OM --> REC["Record run / deliver to origin Channel"]
Cron execution therefore reuses the normal:
- ContextBuilder;
- Provider;
- Tool;
- Session;
- runtime events.
6. Why defer while the Session is active
If a Cron Turn enters while the target Session is handling a user Turn, it can:
- compete with user input;
- reorder history;
- insert a proactive response in the wrong place.
CronTurnCoordinator can use metadata to put the Cron Turn into a deferred queue and publish jobs one by one after the Session becomes idle.
This differs from an ordinary user follow-up in the pending injection queue:
- a follow-up may need to enter the current Turn;
- Cron usually waits until the current user Turn finishes.
7. Heartbeat
Heartbeat:
- is a protected system job;
- reads
<workspace>/HEARTBEAT.md; - focuses on
## Active Tasks; - runs continuously under Gateway;
- uses the same CronService;
- sends useful results to the most recently active chat target;
- cannot be deleted by Cron Tool like an ordinary user job.
Enable it with:
gateway.heartbeat.enabled
For concrete fields, see Gateway Heartbeat configuration.
8. Dream
Dream is also a system job, but it follows a different execution path:
- Gateway’s Cron callback recognizes
job.name == "dream"; - it constructs a Dream prompt and dedicated run directly;
- it does not use an ordinary user-job payload;
- it updates long-term Memory.
Three job types
| Type | Created by | Primary input | Result |
|---|---|---|---|
| User reminder | Cron Tool/user | Session-bound message | returns to original Session/Channel |
| Heartbeat | Gateway system configuration | HEARTBEAT.md | most recently active target |
| Dream | Gateway system configuration or /dream | Memory history + long-term files | updates long-term Memory |
Runtime requirement
These features require a continuously running nanobot gateway:
- scheduled user Cron jobs;
- Heartbeat;
- periodic Dream;
- long-lived Channel connections;
- WebUI/WebSocket.
Running only nanobot agent -m does not keep a background scheduler alive.
Observation task
- Start an isolated Gateway.
- Create a short-delay reminder.
- Inspect
<workspace>/cron/jobs.json. - Inspect its run record.
- Start a long request in the original Session and observe whether Cron is deferred.
- Verify that system jobs are protected.
- Stop Gateway and confirm that background jobs do not run by themselves.
Source and tests
nanobot/cron/types.pynanobot/cron/service.pynanobot/cron/bound_runner.pynanobot/cron/session_turns.pynanobot/agent/cron_turns.pynanobot/cli/commands.py::_run_gatewaytests/cron/tests/agent/test_loop_cron_timezone.pytests/command/test_builtin_dream.py
Self-check
- Why must the Cron store live in the workspace?
- Why must a
jobs.jsonparse failure not become an empty job list followed by a save? - Why does a user Cron job need its origin Channel and Session?
- How do Heartbeat permissions differ from an ordinary reminder?
- How does the Cron deferred queue differ from user pending injection?
Next step
Read Subagent lifecycle.