# `jan cron`
Daemon-backed scheduler for `cron:` script leaves. The daemon ticks every **100 ms**.
`jan cron start` / `stop` manage the daemon; `--list` reads its in-memory schedule cache
(not the YAML tree on disk).
```bash
jan cron start # start daemon (systemd when available)
jan cron stop # stop daemon
jan cron status
jan cron refresh # reload cache after editing YAML cron:
jan cron disable ping-agent # pause an agent (cron + event wakeups)
jan cron enable ping-agent
jan cron disabled # list disabled leaf names
jan cron --list # cached schedules (daemon must be running)
jan cron --list --at "2026-08-05 09:00"
jan cron --dry-run --at "2026-08-05 10:30:00" # one-shot; no daemon
jan cron --help
```
## Lifecycle
| `start` | Start the daemon; installs `~/.config/systemd/user/jan-cron.service` when `systemctl --user` is available, otherwise starts a background process |
| `stop` | Stop the daemon and disable/remove the systemd user service |
| `status` | Report pid, uptime, schedule count, disabled count, events.sock state, wakeup drops/reconnects, running/deferred jobs, and preferred tree |
| `refresh` | Reload the in-memory schedule cache from the preferred YAML tree (`reload` is an alias) |
| `disable <agent>` | Persistently disable cron ticks and Unifier event wakeups for a script leaf |
| `enable <agent>` | Re-enable a disabled agent |
| `disabled` | List disabled agent leaf names |
## Disable / enable
`<agent>` is the script **leaf name** (`ping-agent`) or a chain (`scripts agents ping-agent`).
Disabled agents are skipped for:
- scheduled `cron:` ticks
- Unifier mailbox / named-event wakeups
- one-shot `jan cron --at` runs
State is stored in `$JAN_CONFIG_DIR/cron-disabled.json` (default `~/.config/jan-cli/cron-disabled.json`) and survives `refresh` / daemon restarts. `jan cron --list` marks disabled entries with `[disabled]`.
```bash
jan cron disable ping-agent
jan cron disable scripts agents mute-tracker
jan cron disabled
jan cron enable ping-agent
```
## Schedule cache
On start (and on `refresh`), the daemon walks the preferred tree once and stores every
script with a `cron:` field. The tick loop and `jan cron --list` use that cache only.
After editing `cron:` in YAML, run `jan cron refresh`.
Control socket: `$XDG_RUNTIME_DIR/jan-cli/cron.sock` (fallback: `$JAN_CONFIG_DIR/run/cron.sock`).
## Unifier event wakeups
The daemon also connects to Unifier's `.daemon/events.sock` (under `$UNIFIER_HOME` or
`~/.local/unifier`) and keeps reconnecting while Unifier is down.
| `{"kind":"mailbox","to":"…","id":"…",…}` | Script leaf whose **name** equals `to` | `--message-id <id>`; `JAN_UNIFIER_MESSAGE_ID`, `JAN_UNIFIER_FROM`, `JAN_UNIFIER_TO`, `JAN_UNIFIER_KIND=mailbox` |
| `{"kind":"event","name":"…","id":"…"}` | Script leaf whose **name** equals `name` | `--event-id <id>` (and `--event-name`); `JAN_UNIFIER_EVENT_ID`, `JAN_UNIFIER_EVENT_NAME`, `JAN_UNIFIER_KIND=event` |
Wakeups are queued (cap 256; overflow drops oldest and increments `drops=` in
status) and spawned on the **next 100 ms tick** (same path as cron matches),
subject to concurrency limits:
| `JAN_CRON_MAX_CONCURRENT` | `32` | Max simultaneous agent processes |
| `JAN_CRON_MAX_DEFERRED` | `256` | Max jobs waiting for a free slot |
| `JAN_CRON_ALLOW_OVERLAP` | off | If `1`/`true`, allow the same leaf to run twice at once |
By default, a second wake for a leaf that is already running is skipped
(`overlap_skips=`). Tick-phase notices (`kind: tick`) are not agent wakeups.
The leaf does not need a `cron:` field — only a matching script name. Disabled leaves are skipped.
```bash
unifier daemon start
jan cron start -v
# Sender (often itself a cron agent):
unifier message --from ping-agent pong-agent '{"hello":"world"}'
# Jan invokes:
# jan --no-log scripts agents pong-agent run --message-id <uuid>
```
Start Unifier before (or alongside) Jan so the socket exists; Jan will wait and reconnect.
## One-shot testing (no daemon)
`--dry-run` and `--at` (without `--list`) evaluate schedules in-process:
```bash
jan cron --dry-run --at "2026-08-05 10:30" # minute match
jan cron --dry-run --at "2026-08-05 10:30:45" # second match
jan cron --at "2026-08-05 10:30" # run matches once
```
## Schedule granularity
See [spec/cron.md](../spec/cron.md) for five-, six-, and seven-field expressions.
Prefer `jan use` so the daemon can resolve your tree.