Lights-Out Scheduling
Lights-out scheduling turns AgentXchain from a tool you invoke into a system that runs itself. You configure schedules in agentxchain.json, start the daemon, and governed runs execute on cadence without a human typing agentxchain run.
If you want the end-to-end operator runbook, start with Lights-Out Operation. This page stays focused on the scheduler contract and config surface.
This is the narrow, repo-local first step toward the strategic end state: dark software factories where governed agent teams operate over long horizons with human oversight, not human steering.
When to use scheduling
Use scheduling when you want agents to:
- run nightly code reviews, refactoring passes, or maintenance sweeps
- execute governed test/QA cycles on a fixed cadence
- process accumulated intake signals (CI failures, git changes) without waiting for a human to trigger a run
- keep a governed project moving forward while the team sleeps
Scheduling is not a replacement for agentxchain run. It is the autonomous layer on top of it. Everything the daemon does, you could do manually — the daemon just does it on a clock.
Configuration
Schedules live in agentxchain.json under a top-level schedules object:
{
"project": "my-project",
"roles": { ... },
"workflow": { ... },
"schedules": {
"nightly_review": {
"enabled": true,
"every_minutes": 1440,
"auto_approve": true,
"max_turns": 10,
"initial_role": "qa",
"trigger_reason": "Nightly governed code review"
},
"hourly_intake_sweep": {
"enabled": true,
"every_minutes": 60,
"auto_approve": true,
"max_turns": 5,
"trigger_reason": "Process accumulated intake signals"
}
}
}
| Field | Default | Description |
|---|---|---|
enabled | true | Whether the schedule is eligible to run |
every_minutes | required | Fixed interval cadence in minutes |
auto_approve | true | Whether scheduled runs auto-approve gates |
max_turns | 50 | Safety limit passed to the governed run |
initial_role | config-driven | Optional first-turn role override |
trigger_reason | schedule:<id> | Human-readable provenance recorded in run history |
You can define multiple schedules with different cadences. Each schedule is identified by its key (e.g., nightly_review, hourly_intake_sweep).
Checking schedule status
Before starting the daemon, verify your schedule configuration:
# List all configured schedules with due status
agentxchain schedule list
# Check a specific schedule
agentxchain schedule list --schedule nightly_review
# Machine-readable output
agentxchain schedule list --json
The list command shows:
- whether each schedule is enabled
- when it was last run
- when it is next due
- any skip reasons from the last evaluation
Running due schedules
You can run due schedules without starting the daemon:
# Evaluate all schedules once and run any that are due
agentxchain schedule run-due
# Run only a specific schedule if it is due
agentxchain schedule run-due --schedule nightly_review
This is useful for:
- cron-based execution (run
agentxchain schedule run-duefrom a system cron job) - CI-triggered runs (run it from a GitHub Action or other CI system)
- manual one-off evaluation
Starting the daemon
The daemon polls for due schedules on a fixed interval and runs them automatically:
# Start with default 60-second poll interval
agentxchain schedule daemon
# Custom poll interval
agentxchain schedule daemon --poll-seconds 300
# Limit the number of poll cycles (useful for testing)
agentxchain schedule daemon --max-cycles 10
# Run only a specific schedule
agentxchain schedule daemon --schedule nightly_review
The daemon runs in the foreground. To run it in the background:
# Background with nohup
nohup agentxchain schedule daemon --poll-seconds 300 > schedule.log 2>&1 &
# Or use tmux/screen for a persistent session
tmux new-session -d -s agentxchain-daemon 'agentxchain schedule daemon --poll-seconds 300'
Blocked scheduled runs are not auto-recovered. The daemon keeps polling but does not invent a restart. Resolve the blocker with the recovery action surfaced by the governed state — for example, agentxchain unblock <id> for needs_human, agentxchain reissue-turn --reason ghost for a BUG-51 ghost-startup failure, or agentxchain reissue-turn --reason stale for a BUG-47 stalled subprocess. The schedule daemon emits the actual recovery_action in schedule run-due --json and schedule daemon --json (blocked_category, recovery_action); agentxchain status and the terminal Schedule blocked: <id> (<category>). Recovery: <recovery_action> line both show the right command. On the next poll after the blocker is cleared, the daemon continues that same schedule-owned run automatically.
Daemon health monitoring
The daemon writes a heartbeat file at .agentxchain/schedule-daemon.json on every poll cycle. Check its health:
agentxchain schedule status
| Status | Meaning |
|---|---|
running | Heartbeat is recent (within poll_seconds * 3 or 30 seconds, whichever is greater) |
stale | Heartbeat file exists but is older than the staleness threshold |
not_running | State file exists but is malformed or missing heartbeat data |
never_started | No daemon state file found |
For automation:
agentxchain schedule status --json
Returns pid, started_at, last_heartbeat_at, last_cycle_result, poll_seconds, and stale_after_seconds.
The doctor command also checks daemon health when schedules are configured:
agentxchain doctor
Safety behavior
Scheduled runs follow strict safety rules:
-
No double-runs. If the project is already
activeorpaused, the scheduler records a skip reason and moves on. It does not start a second concurrent run. -
No hijacking. If a run is
blocked(waiting for operator recovery), the scheduler does not auto-recover it. A human must explicitly recover blocked runs. -
Start from clean state only. Scheduled runs start only from a fresh repo with no run state yet, or from
idle/completedstatus. -
Provenance tracking. Scheduled runs use
trigger: "schedule"in run history, so you can always distinguish scheduled runs from manual ones. -
Budget enforcement. If
budget.on_limitispause_and_escalate, a scheduled run that exhausts its budget will block and escalate — the daemon will skip it on subsequent cycles until an operator intervenes. Ifbudget.on_limitiswarn, the run continues past budget with observable warnings.
Schedule-owned continuous mode
A schedule can opt into continuous vision-driven execution by adding a continuous block. Instead of running a single governed cycle per cadence, the daemon maintains a persistent session that chains governed runs back-to-back, deriving work from your project's VISION.md when the intake queue is empty.
{
"schedules": {
"vision_autopilot": {
"enabled": true,
"every_minutes": 60,
"auto_approve": true,
"max_turns": 10,
"initial_role": "pm",
"continuous": {
"enabled": true,
"vision_path": ".planning/VISION.md",
"max_runs": 100,
"max_idle_cycles": 8,
"on_idle": "perpetual",
"triage_approval": "auto",
"per_session_max_usd": 500.0,
"idle_expansion": {
"sources": [".planning/VISION.md", ".planning/ROADMAP.md", ".planning/SYSTEM_SPEC.md"],
"max_expansions": 5,
"role": "pm"
}
}
}
}
}
| Field | Default | Description |
|---|---|---|
continuous.enabled | false | Opt the schedule into continuous mode |
continuous.vision_path | — | Required when enabled. Project-relative path to the VISION file |
continuous.max_runs | 50 | Max governed runs before the session exits terminally |
continuous.max_idle_cycles | 5 | Max daemon polls with no derivable work before the on_idle policy fires |
continuous.on_idle | "exit" | exit, perpetual, or human_review after the idle threshold |
continuous.idle_expansion.* | see CLI reference | PM idle-expansion sources, role, malformed-output retry budget, and max expansion cap for on_idle: "perpetual" |
continuous.triage_approval | "auto" | "auto" auto-approves vision-derived intents; "human" pauses for operator triage |
continuous.per_session_max_usd | null | Cumulative session-level budget cap in USD. When total spend across all runs reaches this limit, the session stops cleanly. null disables |
How it works
- When the schedule is due, the daemon starts a schedule-owned continuous session and records
owner_type: "schedule"plusowner_idin.agentxchain/continuous-session.json. - On each subsequent poll, the daemon advances the session by one step — even if the schedule's
every_minuteshasn't elapsed again. Due-ness gates session creation, not session continuation. - Each step checks the intake queue first. If queued work exists (including injected
p0priorities), it runs that. Otherwise, it seeds a new intent fromvision_path. - Work flows through the real intake lifecycle:
planIntent()→startIntent()→ governed run →resolveIntent(). - The session exits terminally when
max_runsis reached,max_idle_cyclesconsecutive polls find no work, cumulative spend reachesper_session_max_usd, or the operator stops it.
Blocked recovery
If a continuous session blocks, the daemon records continuous_blocked and keeps polling. The recovery command depends on what blocked the run — agentxchain unblock <id> for needs_human, agentxchain reissue-turn --reason ghost for a BUG-51 ghost-startup failure, or agentxchain reissue-turn --reason stale for a BUG-47 stalled subprocess. The actual recovery_action and blocked_category flow through schedule daemon --json and agentxchain status. After the blocker is resolved, the next poll resumes the same session — no re-launch needed.
Priority injection precedence
If a schedule-owned continuous run yields priority_preempted, the daemon consumes the injected p0 work before any new vision seeding. Human priorities always take precedence over vision-derived work.
Visibility
agentxchain status --json reports the active schedule-owned continuous session:
{
"continuous_session": {
"session_id": "cont-abc12345",
"status": "running",
"vision_path": ".planning/VISION.md",
"runs_completed": 7,
"max_runs": 100,
"owner_type": "schedule",
"owner_id": "vision_autopilot",
"current_vision_objective": "Goals: Build the API"
}
}
Standalone vs daemon-owned continuous mode
Both run --continuous --vision and schedule-owned continuous mode use the same advanceContinuousRunOnce() step primitive. The difference is who owns the outer loop:
run --continuous— the CLI process owns cadence with sleep between stepsschedule daemon— the daemon owns cadence with its existing poll loop
The two surfaces share one code path and cannot drift semantically.
Combining with intake
Scheduling pairs naturally with the continuous delivery intake surface. A common pattern:
- Intake records signals continuously — CI failures, git ref changes, manual records
- A scheduled run processes accumulated intents — the initial role triages, plans, and executes work from the intake backlog
- The daemon ensures this happens on cadence — no human needs to remember to run it
Example configuration:
{
"schedules": {
"process_intake": {
"enabled": true,
"every_minutes": 120,
"auto_approve": true,
"max_turns": 15,
"initial_role": "pm",
"trigger_reason": "Process intake backlog every 2 hours"
}
}
}
Multi-repo boundary
This scheduling surface is repo-local only. It runs inside a governed project rooted by agentxchain.json. It does not run from a coordinator workspace rooted by agentxchain-multi.json, and it does not fan out across child repos for you.
If you need multi-repo automation today, schedule each child governed repo independently, then reconcile coordinator state separately with agentxchain multi step.
See Multi-Repo Orchestration for the coordinator surface. Do not treat schedule daemon as a coordinator scheduler. That capability does not ship today.
Monitoring scheduled runs
After the daemon runs governed cycles, inspect the results:
# See run history including scheduled runs
agentxchain history
# Filter for schedule-triggered runs
agentxchain events --json | grep '"trigger":"schedule"'
# Compare two scheduled runs
agentxchain diff <run_id_1> <run_id_2>
Operational patterns
Nightly code review
{
"schedules": {
"nightly_review": {
"every_minutes": 1440,
"initial_role": "qa",
"max_turns": 8,
"trigger_reason": "Nightly code quality review"
}
}
}
Hourly CI failure triage
{
"schedules": {
"ci_triage": {
"every_minutes": 60,
"initial_role": "pm",
"max_turns": 5,
"trigger_reason": "Triage CI failures from intake"
}
}
}
Weekly maintenance sweep
{
"schedules": {
"weekly_maintenance": {
"every_minutes": 10080,
"initial_role": "dev",
"max_turns": 20,
"trigger_reason": "Weekly dependency updates and cleanup"
}
}
}
What this is not
Scheduling is repo-local and daemon-based. It is not:
- a hosted orchestration service (that is the future
.aisurface) - a distributed job queue
- a coordinator-workspace scheduler
- a replacement for CI/CD pipelines
- an auto-recovery mechanism for blocked runs
It is the simplest possible path from "I run agentxchain run manually" to "AgentXchain runs governed cycles on its own." Everything more sophisticated belongs in the managed cloud layer.