Skip to content

Scheduler and Cron

refact-planner edited this page Jun 7, 2026 · 1 revision

Scheduler and Cron

Cron scheduling, durable/session storage, jitter, catch-up, and fire injection for the agent runtime.

See also Exec Runtime and Chat System.

The scheduler owns cron-style scheduled tasks and is spawned from background tasks only when enabled. It supports both session-only tasks and durable tasks stored per project at <project>/.refact/scheduled_tasks.json.

Core behavior

  • Session jobs live in the in-memory scheduler store and disappear on engine restart.
  • Durable jobs are persisted per project and survive restarts.
  • The scheduler uses a 5-field cron expression in local time.
  • Jitter is applied automatically to spread load, but it does not change the stored cron expression.
  • Due jobs do not inject while the owning chat is Generating, ExecutingTools, or Paused; they defer and re-check later.
  • Recurring durable jobs compute their next future fire from now instead of replaying a backlog burst.
  • Past one-shot durable jobs fire ASAP and may mark the fire payload as missed.
  • Recurring jobs default to a 30-day auto-expiration window and final fires carry final=true before deletion.

Kill switches

Scheduler startup and cron effects are skipped when any of the following are set:

  • REFACT_DISABLE_SCHEDULER=1
  • --no-scheduler
  • scheduler.enabled: false in global config

A separate config flag, scheduler.disable_durable: true, makes cron_create fall back to session-only and return the note durable schedules disabled by config.

Tools

cron_create

Schedule a prompt to be enqueued later.

Schema:

{
  "type": "object",
  "properties": {
    "cron": { "type": "string", "description": "Standard 5-field cron expression in local time." },
    "prompt": { "type": "string", "description": "Prompt enqueued at each fire time." },
    "recurring": { "type": "boolean", "default": true },
    "durable": { "type": "boolean", "default": false },
    "description": { "type": "string", "description": "Short description (≤80 chars) shown in cron_list UI." }
  },
  "required": ["cron", "prompt", "description"]
}

Returns { id, human_schedule, recurring, durable } and emits event(system_notice, "scheduler.cron", {id, cron, recurring, durable}, summary).

Notes:

  • Rejects invalid cron expressions.
  • Rejects expressions with no match within a year.
  • Rejects descriptions over 80 chars.
  • Rejects durable jobs when there is no project root.
  • Rejects jobs beyond the configured cap.

cron_list

List scheduled tasks, optionally filtering by session-only or durable scope.

Schema:

{
  "type": "object",
  "properties": {
    "scope": { "type": "string", "enum": ["session", "durable", "all"], "default": "all" }
  },
  "required": []
}

Returns an array of:

{
  "id": "...",
  "cron": "0 9 * * 1-5",
  "human_schedule": "...",
  "description": "...",
  "prompt": "...",
  "recurring": true,
  "durable": true,
  "next_fire_at_ms": 0,
  "fire_count": 0,
  "created_at_ms": 0
}

The prompt is truncated to the first 200 characters.

cron_delete

Cancel a scheduled task by ID.

Schema:

{
  "type": "object",
  "properties": { "id": { "type": "string" } },
  "required": ["id"]
}

Returns { "removed": boolean } and notifies the runner to recompute wakeups.

Cron fire injection

On fire, the runner appends:

{
  "role": "event",
  "content": "...",
  "extra": {
    "event": {
      "subkind": "cron_fire",
      "source": "scheduler.cron",
      "payload": {
        "task_id": "...",
        "cron": "...",
        "recurring": true,
        "fire_count": 1,
        "final": false,
        "missed": false
      }
    }
  }
}

It also enqueues a ChatCommand::UserMessage with the configured prompt so the agent wakes up.

Clone this wiki locally