Skip to main content

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"
}
}
}
FieldDefaultDescription
enabledtrueWhether the schedule is eligible to run
every_minutesrequiredFixed interval cadence in minutes
auto_approvetrueWhether scheduled runs auto-approve gates
max_turns50Safety limit passed to the governed run
initial_roleconfig-drivenOptional first-turn role override
trigger_reasonschedule:<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-due from 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
StatusMeaning
runningHeartbeat is recent (within poll_seconds * 3 or 30 seconds, whichever is greater)
staleHeartbeat file exists but is older than the staleness threshold
not_runningState file exists but is malformed or missing heartbeat data
never_startedNo 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:

  1. No double-runs. If the project is already active or paused, the scheduler records a skip reason and moves on. It does not start a second concurrent run.

  2. 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.

  3. Start from clean state only. Scheduled runs start only from a fresh repo with no run state yet, or from idle / completed status.

  4. Provenance tracking. Scheduled runs use trigger: "schedule" in run history, so you can always distinguish scheduled runs from manual ones.

  5. Budget enforcement. If budget.on_limit is pause_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. If budget.on_limit is warn, 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"
}
}
}
}
}
FieldDefaultDescription
continuous.enabledfalseOpt the schedule into continuous mode
continuous.vision_pathRequired when enabled. Project-relative path to the VISION file
continuous.max_runs50Max governed runs before the session exits terminally
continuous.max_idle_cycles5Max 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 referencePM 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_usdnullCumulative session-level budget cap in USD. When total spend across all runs reaches this limit, the session stops cleanly. null disables

How it works

  1. When the schedule is due, the daemon starts a schedule-owned continuous session and records owner_type: "schedule" plus owner_id in .agentxchain/continuous-session.json.
  2. On each subsequent poll, the daemon advances the session by one step — even if the schedule's every_minutes hasn't elapsed again. Due-ness gates session creation, not session continuation.
  3. Each step checks the intake queue first. If queued work exists (including injected p0 priorities), it runs that. Otherwise, it seeds a new intent from vision_path.
  4. Work flows through the real intake lifecycle: planIntent()startIntent() → governed run → resolveIntent().
  5. The session exits terminally when max_runs is reached, max_idle_cycles consecutive polls find no work, cumulative spend reaches per_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 steps
  • schedule 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:

  1. Intake records signals continuously — CI failures, git ref changes, manual records
  2. A scheduled run processes accumulated intents — the initial role triages, plans, and executes work from the intake backlog
  3. 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 .ai surface)
  • 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.