?

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

  1. Scheduler re-syncs the job list from SQLite every PollInterval.
  2. robfig/cron/v3 fires the job at its scheduled time.
  3. 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).
  4. The reply text is passed to Delivery(ctx, job.Target, replyText).
  5. Delivery returns 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.

Type to search 150+ pages. Ranking: BM25 with title/description boost.