Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scheduling & Daemon

Scoot can run unattended scheduled jobs through a foreground daemon loop. Autonomy is off by default — you must explicitly enable it. The full lifecycle/recovery reference is docs/DAEMON.md.

Which Mode Should I Use?

Use this table before choosing between -e, schedule run, and daemon run:

ModeReads jobs from config?Runs forever by default?Typical owner of timingBest fit
scoot -e "<goal>"nonocallerOne immediate human/scripted task.
scoot schedule run --ticks 1yesnocron, systemd timer, CIExternal scheduler triggers Scoot periodically.
scoot schedule runyesyescurrent terminal/process managerSimple foreground scheduler loop without daemon state files.
scoot daemon runyesyesScoot loop plus systemd/launchd/etc. supervisionLong-running unattended scheduler with pid/state/stop/status support.

-e and scheduled execution are different entry points. -e runs the prompt you pass on the command line immediately, using the normal configured tool policy. Scheduled jobs come from [[schedule.jobs]], are triggered by every_sec, at_unix, or cron, and use the unattended safety rule: job mode defaults to readonly, and guarded is coerced to effective readonly.

systemd is useful only when you want a process supervisor. With scoot daemon run, Scoot owns the schedule loop while systemd owns startup, restart, logs, environment, resource limits, and SIGTERM shutdown. If you want systemd to own the timing too, use a systemd timer that invokes scoot schedule run --ticks 1.

Enable Scheduling

[schedule]
enabled = true
poll_ms = 1000

[[schedule.jobs]]
id = "disk-check"
goal = "Inspect disk usage and summarize anomalies"
every_sec = 300
mode = "readonly"

Each job needs exactly one trigger:

TriggerMeaning
every_secFire on a fixed interval (seconds).
at_unixFire once at a fixed Unix-time instant.
cronFire on a 5-field UTC cron expression.

A job with zero or multiple triggers is invalid and skipped with a warning. See Configuration → [[schedule.jobs]] for every field.

Unattended Safety

Scheduled jobs enforce safety structurally, not by convention:

  • a job’s mode defaults to readonly;
  • a guarded job is coerced to effective readonly at execution time;
  • unrestricted only takes effect if you set it explicitly, accepting the unattended write/network risk.

This means an unattended job cannot accidentally write or hit the network unless you deliberately opted in. See Execution Policy & Security.

Running The Scheduler

scoot schedule list             # show jobs and whether each is ACTIVE/INACTIVE
scoot schedule run              # run the loop in the foreground
scoot schedule run --ticks 1    # run exactly one poll cycle, then exit

--ticks N is handy for testing and cron-driven one-shot invocation: it polls N times and exits (0 = run forever).

Daemon Mode

daemon is the long-running foreground process for scheduled jobs. It does not fork into the background — pair it with systemd, launchd, tmux, or a shell job for background ownership.

scoot daemon run                # foreground; requires schedule.enabled = true
scoot daemon run --ticks 3      # run three poll cycles then exit
scoot daemon status             # print the last recorded daemon state
scoot daemon stop               # SIGTERM only when running state and pid agree

daemon run loads valid jobs, writes lifecycle state, installs SIGTERM/SIGINT handlers, and runs the same loop as schedule run. On stop, Scoot only signals when state/daemon.json says running and matches state/daemon.pid; otherwise the pid file is treated as stale. A running daemon finishes the current tick, writes a stopped state, and removes its pid file.

One Daemon Per Runtime Directory

Daemon liveness is tracked per runtime directory through state/daemon.json and state/daemon.pid. Starting daemon run while another daemon for the same directory is still alive is refused, so two daemons can never share one schedule and state tree:

[scoot] refusing to start: detected daemon already running (pid=… started_at=…).
Run `scoot daemon stop` first.

The guard probes the recorded pid with signal 0; a stale pid left by a crash is treated as an unclean stop and recovered on the next run.

To run several daemons on one host, give each its own runtime directory and they stay fully isolated — separate config, jobs, sessions, logs, and lifecycle files:

scoot --scoot-home /opt/scoot/web   setup     # provision instance "web"
scoot --scoot-home /opt/scoot/batch setup     # provision instance "batch"

SCOOT_HOME=/opt/scoot/web   scoot daemon run &
SCOOT_HOME=/opt/scoot/batch scoot daemon run &

scoot setup is the quickest way to provision each directory. Because the single-daemon guard is per directory, distinct homes never collide.

Lifecycle Files

~/.scoot/
  logs/audit.jsonl       # audit events
  state/daemon.json      # status, pid, timestamps, stop reason, job count, poll interval
  state/daemon.pid       # present while running; removed on clean shutdown
  state/sessions/        # per-run session transcripts

If the process crashes, the next daemon run notices the previous state was still running and prints a restart-recovery warning before writing a fresh state.

Recovery Contract

Recovery is intentionally conservative — Scoot does not resume an in-progress model turn after process death:

  • completed sessions remain in state/sessions/;
  • already-flushed audit events remain in logs/audit.jsonl;
  • every_sec / at_unix runtime timers reset on restart;
  • config remains the source of truth for which jobs exist;
  • a stale running state is treated as an unclean stop and overwritten.

Example: a systemd unit

[Unit]
Description=Scoot daemon
After=network-online.target

[Service]
ExecStart=/usr/local/bin/scoot daemon run
Restart=on-failure
Environment=SCOOT_HOME=%h/.scoot

[Install]
WantedBy=default.target

Log and session files are append-only in this release; rotate or prune logs/ and state/sessions/ externally for long-running deployments.