Skip to main content
loop cron installs a schedule into a real scheduler and never runs one itself — loop.js owns no scheduler and no daemon. An installed schedule is an Entry; when it fires, that tick is a Trigger that runs loop run in the project directory. A tick is a scheduler firing; a Run is one execution of loop run — a tick may skip or expire without running, so the two are counted apart.

Lifetime: --until

An Entry declares its lifetime at add--until settled or --until forever, no default; an add without --until is refused with an error naming both. --until settled — the Entry removes itself at the first of: the Loop settling (ok or give-up; the settling tick removes its own Entry), its --max-runs-th run of loop run (default 3), or --expires after install (default 24h) — a tick past the expiry removes the Entry instead of running. On settled the caps are always on — resizable, never removable — because they bound the path where no Verdict ever comes. --until forever — the Entry stays until loop cron remove. Each tick on a settled Loop re-judges it through the Verify gate, which is what lets a time-dependent Goal (“today’s report exists”) go stale and re-open daily. The same caps opt in as safety bounds; a settled exit never removes a forever Entry — only a cap or remove does. --expires takes a duration: a whole number with a unit — 45s, 90m, 36h, 7d.

Backends: --backend

local installs into the OS’s own scheduler, chosen by platform: crontab on Linux and other unix, launchd on macOS (cron there skips ticks while the machine sleeps and sits behind Full Disk Access), and Task Scheduler (schtasks) on Windows.
add deploys one Modal App per Entry, carrying a modal.Cron on your expression; each tick fires an ephemeral Runner that runs loop run against the Volume. You bring your own Modal token — pass --token-id/--token-secret, set MODAL_TOKEN_ID/MODAL_TOKEN_SECRET, or have run modal token set; loop.js runs no OAuth of its own. What an Entry deploys, and what each part holds: The rules that follow from that split:
  • remove deletes the Entry, never its Volume — removing a schedule never destroys State. (The lock Dict goes with the Entry.)
  • The Secret is created only when absent, never overwritten. add harvests the key from its environment into the one shared loop-js Secret every Entry’s function mounts. Rotation is one modal secret create loop-js --force … — no Entry redeploys.
  • Overlap is skip-only: a tick that finds a fresh claim in the Dict prints one line and exits 0; a stale claim is taken over, same as the engine’s own Lock. Volume commits are last-write-wins, which is why the lock lives in the Dict and only State rides the Volume.
  • A tick runs with Modal’s maximum Function timeout (24h) — on this backend the platform ceiling is the outer guard on Run duration.
  • On --until settled, the settling tick stops its own App — the Entry removes itself in the cloud exactly as it does locally.

list and remove

list shows one line per Entry — <id> <expr> <dir> <lifetime>, the lifetime in plain words such as until-settled max-runs=3 expires=24h or forever. remove <id> deletes the Entry; it exits 1 if no Entry has that id.