Overview
The cron scheduler (internal/cron/scheduler.go) is a goroutine that runs stored CronJob entries on their configured schedule, executes each job's prompt through the agent, and hands the reply to a transport-agnostic Delivery function.
The scheduler runs alongside any long-running daemon (typically rousseau whatsapp or another chat transport). Jobs are stored in the same SQLite database as sessions, so they survive restarts.
Schedule syntax
Backed by robfig/cron/v3. The parser supports:
- Standard 5-field cron:
<minute> <hour> <day-of-month> <month> <day-of-week>. - Predefined shortcuts:
@yearly,@monthly,@weekly,@daily,@hourly,@every <duration>.
Example schedules:
| Expression | Fires |
|---|---|
0 9 * * 1-5 |
09:00 on weekdays |
*/15 * * * * |
Every 15 minutes |
@daily |
Once a day at midnight (server timezone) |
@every 30m |
Every 30 minutes |
CLI
# List all stored jobs.
rousseau cron list
# Add a job.
rousseau cron add \
--name morning-standup \
--schedule '0 9 * * 1-5' \
--prompt 'What are the top three engineering priorities today?' \
--target '447900123456@s.whatsapp.net'
# Remove by name or ID.
rousseau cron remove morning-standup
Configuration
Jobs are stored in the state DB, not the config file. There is nothing in ~/.config/rousseau/config.yaml to configure the scheduler itself; it uses default PollInterval = 60s.
Job flow
- Scheduler re-syncs the job list from SQLite every
PollInterval. robfig/cron/v3fires the job at its scheduled time.TurnRunner.RunOnce(ctx, job.Prompt)executes a single-turn agent run against a fresh session (no history, no cross-session recall unless the runner opts in).- The reply text is passed to
Delivery(ctx, job.Target, replyText). Deliveryreturns error → logged; the next tick retries.
Delivery
Delivery is a small function type:
type Delivery func(ctx context.Context, target, body string) error
The scheduler does not import internal/transport — the delivery contract is transport-agnostic. In practice, rousseau <transport> daemons wire a Delivery that resolves the target string against the active transport (Deliver on the transport client).
target is transport-specific:
- WhatsApp: a JID (
447900123456@s.whatsapp.net). - Telegram: a numeric chat ID.
- Slack: a channel ID (
C012345) or user ID (U012345). - Discord: a channel ID.
- SMS: an E.164 destination.
- iMessage: a chat GUID.
- Signal: an E.164 destination.
- Matrix: a room ID.
- Email: a full RFC 5322 address.
Persistence
Jobs are stored in the cron_jobs table of the state database (internal/state/sqlite/). Fields: id, name, schedule, prompt, target, created_at, updated_at. Restarts pick up every job on the next PollInterval.
New jobs added via rousseau cron add become live within one PollInterval — up to 60 seconds by default.
Interaction with transports
The Delivery closure captures a reference to the running transport. A single daemon typically runs one transport, so the cron scheduler delivers through that transport. Multi-transport deployments run one daemon per transport, and the operator points each cron job's target at the matching transport's daemon.
Cross-transport delivery (job runs in the WhatsApp daemon, replies via Slack) is not supported today — the scheduler only knows about the Delivery it was given.
Failure modes
| Symptom | Fix |
|---|---|
| Job doesn't fire | Check rousseau status; the scheduler logs cron.fired per activation. |
| Job fires but nothing arrives | Delivery error — check logs for cron.delivery_failed. |
| Job runs but the model refuses to act | Approval policy denying tool calls. Loosen agent.approver or move to pattern mode. |
| Delivery goes to wrong target | The scheduler is transport-agnostic; the daemon interprets target. Confirm the transport your daemon is running matches the target format. |