Cron System
Status
Implemented — internal/cron/ package with gocron/v2 scheduler, JSON persistence, and agent tool.
Overview
Anna supports scheduled task execution so the agent can set reminders, run periodic tasks, and automate recurring work. The cron system delegates all scheduling to gocron/v2 and adds persistence and an agent-facing tool on top.
Architecture
Agent (via tool call)
|
| add / list / remove
v
+----------+ +-------------+
| CronTool | ----> | Service |
+----------+ +------+------+
|
+-----------+-----------+
| |
gocron/v2 Scheduler jobs.json (disk)
|
v
OnJobFunc callback
|
v
pool.Chat(ctx, "cron:{id}", message)Package: internal/cron/
Top-level package (under internal/). Five files:
| File | Purpose |
|---|---|
internal/cron/job.go | Job and Schedule types |
internal/cron/service.go | Service — gocron wrapper, scheduling, job CRUD |
internal/cron/heartbeat.go | Heartbeat polling — decide/execute/notify via LLM |
internal/cron/persistence.go | JSON file I/O (load/save jobs) |
internal/cron/tool.go | CronTool — agent tool implementing tool.Tool |
Key Types
Schedule defines when a job runs. Exactly one field must be set:
cron— a cron expression (e.g."0 9 * * 1-5"for weekdays at 9am)every— a Go duration (e.g."30m","2h","24h")at— an RFC3339 timestamp for a one-time job (e.g."2024-01-15T14:30:00+08:00")
Job is the persisted definition:
type Job struct {
ID string // short UUID
Name string // human-readable name
Schedule Schedule // cron, interval, or one-time
Message string // prompt sent to agent
SessionMode string // "reuse" (default) or "new"
Enabled bool
CreatedAt time.Time
}Service Lifecycle
cron.New(dataPath)— creates schedulerservice.SetOnJob(fn)— sets callback (deferred wiring to resolve circular dependency)service.Start(ctx)— loadsjobs.json, registers all jobs with gocron, starts schedulerservice.Stop()— shuts down scheduler
Persistence
Jobs are stored as a JSON array in {dataDir}/jobs.json (default: ~/.anna/workspace/cron/jobs.json). Writes are atomic (temp file + rename).
One-Time Jobs
Jobs scheduled with at run exactly once at the specified time and are automatically removed from both the scheduler and jobs.json after execution. This keeps the job list clean without stale entries.
Behavior details:
- The
atfield must be a valid RFC3339 timestamp with timezone offset - Timestamps in the past are rejected at creation time
- If Anna restarts and a one-time job's timestamp has already passed, the job is silently skipped (not scheduled) but remains in persistence until manually removed
- On successful execution, the cleanup runs asynchronously to avoid blocking the scheduler
Session Model
Each cron job's session behavior is controlled by its session_mode:
reuse(default) — the job gets a stable session IDcron:{job.ID}. The agent retains conversational memory across scheduled runs of the same job.new— each execution gets a unique session IDcron:{job.ID}:{timestamp}. The agent starts fresh every time with no prior context.
Configuration
Add to ~/.anna/config.yaml:
cron:
enabled: true
data_dir: ~/.anna/workspace/cron # optional, this is the defaultCron is only active when:
cron.enabledistruerunner.typeisgo(the Pi runner doesn't support custom tools)
Agent Tool
The cron tool is automatically registered with the Go runner when cron is enabled. The agent uses it via tool calls with three actions:
add — Create a job
Parameters:
name(required) — human-readable namemessage(required) — the instruction to execute on each runcron— cron expression (use this OReveryORat)every— Go duration (use this ORcronORat)at— RFC3339 timestamp for a one-time job (use this ORcronORevery)session_mode—"reuse"(default) keeps conversation history;"new"starts fresh each execution
Example (recurring): "Set a reminder every 30 minutes to check my email" triggers:
{"action": "add", "name": "email check", "message": "Check my email and summarize new messages", "every": "30m"}Example (one-time): "Remind me at 2:40 PM to check Beijing weather" triggers:
{"action": "add", "name": "weather reminder", "message": "Check Beijing weather and send me a summary", "at": "2024-01-15T14:40:00+08:00"}list — List all jobs
No parameters. Returns all scheduled jobs as JSON.
remove — Delete a job
Parameters:
id(required) — job ID fromaddorlist
Heartbeat
Heartbeat is a built-in periodic task managed by the cron service. It polls a HEARTBEAT.md file and uses the LLM to decide whether action is needed, executing instructions and sending results via the notification dispatcher.
How It Works
SetHeartbeat(cfg, chatFn, notifier)configures heartbeat on the cron serviceStartHeartbeat(ctx, every)schedules the poll loop viaScheduleEvery- Each tick:
- Reads the heartbeat file (skips if missing or empty)
- Sends the content to the fast model for a
skip/rundecision (no tools allowed) - On
run, sends the content to the main session for execution - Delivers the result via the notification dispatcher
Configuration
heartbeat:
enabled: false # default: false
every: 10m # poll interval (Go duration)
file: HEARTBEAT.md # relative to workspace unless absoluteHeartbeat only runs in anna gateway mode. The fast model is used for the gate decision to minimize cost.
Wiring
The cron system resolves a circular dependency (service needs pool for the callback, runner needs the tool) via deferred wiring in main.go:
- Create
cron.Servicewith no callback - Create
cron.NewTool(service)and pass to runner viaExtraTools - Create pool with the runner factory
- Call
service.SetOnJob(...)with a callback that callspool.Chat() - If heartbeat is enabled, call
service.SetHeartbeat(...)with the chat function and notifier - Call
service.Start(ctx)(orStartEphemeralfor heartbeat-only mode) in the gateway - Call
service.StartHeartbeat(ctx, every)after channels are wired
Testing
Tests are in internal/cron/cron_test.go and internal/cron/heartbeat_test.go covering:
- Add, list, remove lifecycle
- Input validation (empty name, missing schedule, invalid duration, conflicting schedule fields, invalid/past timestamps)
- Remove non-existent job
- Persistence across service restart
- Callback firing on schedule
- One-time job creation and validation
- One-time job fires exactly once and auto-removes
- One-time job with past timestamp skipped on restart
- Tool interface for one-time jobs
- Session mode default, reuse, new, and invalid validation
- Session mode via tool interface
- Full tool interface (add/list/remove via
Execute) - Error cases (invalid action, missing ID)
- Heartbeat: skip when file is missing
- Heartbeat: fast model used for decision
- Heartbeat: run decision executes and notifies
- Heartbeat: error when decision uses tools
- Heartbeat: notifier errors propagated
Run with:
go test -race ./cron/