> ## Documentation Index
> Fetch the complete documentation index at: https://loop-js.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# loop cron

> Install, list, and remove schedules that run `loop run` — into the OS's own scheduler or Modal.

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

```bash theme={null}
loop cron add "<cron-expr>" --until <settled|forever>
                              install a schedule that runs `loop run` in this dir
loop cron list                show installed schedules with ids and lifetimes
loop cron remove <id>         remove a schedule

  --until settled               the entry removes itself at the Loop's first settle — capped by
                                --max-runs/--expires in case it never settles
  --until forever               keep until `loop cron remove` — each tick on a settled Loop
                                re-judges it through the Verify gate; caps opt in
  --max-runs <n>                at most n runs of `loop run` (settled default: 3; forever: none)
  --expires <duration>          self-remove this long after install — 45s, 90m, 36h, 7d
                                (settled default: 24h; forever: none)
  --backend <local|modal>       where to install (default: local)
  --token-id <id>               the Modal token, for --backend modal; else MODAL_TOKEN_ID
  --token-secret <secret>       …and its other half; else MODAL_TOKEN_SECRET, or `modal token set`
```

## 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`.

```bash theme={null}
loop cron add "*/30 * * * *" --until settled                              # gone at the first settle (capped)
loop cron add "*/30 * * * *" --until settled --max-runs 10 --expires 7d   # same, caps resized
loop cron add "0 8 * * *" --until forever                                 # stays until removed
loop cron add "0 8 * * *" --until forever --expires 30d                   # standing, safety bound opted in
```

## Backends: `--backend`

| Backend           | Where a tick runs                                    | State lives           |
| ----------------- | ---------------------------------------------------- | --------------------- |
| `local` (default) | your machine — the OS's own scheduler                | the project directory |
| `modal`           | the cloud — Modal fires an ephemeral Runner per tick | a Modal Volume        |

**`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.

## Modal: `--backend modal`

```bash theme={null}
loop cron add "0 8 * * *" --until forever --backend modal
```

`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:

| Resource           | Name                    | What it holds                                                              |
| ------------------ | ----------------------- | -------------------------------------------------------------------------- |
| App + `modal.Cron` | `loop-js-<id>`          | the schedule and the per-tick function                                     |
| Image              | baked at `add`          | the project code — code delivery, never State                              |
| Volume             | `loop-js-<id>`          | **State** — the work tree, `.loop/`, `.handoff/`; seeded by the first tick |
| `modal.Dict`       | `loop-js-<id>-lock`     | the tick lock — overlap arbitration off the Volume                         |
| `modal.Secret`     | `loop-js` (one, shared) | the agents' key — `ANTHROPIC_API_KEY` (else `CLAUDE_CODE_OAUTH_TOKEN`)     |

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.

```bash theme={null}
$ loop cron list
3f9a1c2e  0 8 * * *  /home/dev/my-loop  until-settled max-runs=3 expires=24h
$ loop cron remove 3f9a1c2e
removed 3f9a1c2e
```
