# Scheduling

> For the complete documentation index, see [llms.txt](/llms.txt)

A **schedule** is a declaration: *this collection (or this test) runs on this
cron, in this environment, after this preparation*. It is data that travels
with the tests it names — versioned with them, pushed with them — and
nothing in `@unotest/web` ticks: no timer is armed locally, no cron is
parsed. Whoever hosts your runs executes the schedule — a
[box](/box/schedules-and-queue/) or the CI you already have — and
`npx @unotest/web schedules` says so out loud rather than implying a timer
that does not exist.

## Where schedules live

Two halves, one merged list:

- **`schedules` in `unotest.config`** — hand-written entries. Each names a
  `collection`, a 5-field `cron`, and optionally an `env` (the `.env.<name>`
  overlay, with its own `.runs.<name>` history) and a `prepare` shell
  command that runs before the collection inside the same queue slot —
  seeding a database, resetting a fixture. See the
  [config reference](/reference/config/).
- **`unotest/schedules.yaml`** — the half the [viewer edits](/viewer/running/#schedule)
  when you schedule a test or a collection from the tree:

  ```yaml
  schedules:
    - collection: smoke
      cron: "0 * * * *"
    - scenario: checkout/guest
      cron: "0 3 * * *"
      env: staging
  ```

  An entry names either a `collection:` or a single test as `scenario:
  <ref>`, plus `cron:`; `env:` and `prepare:` are optional. A missing file
  means no viewer-managed schedules; a file that exists but cannot be read
  is an error, never silently "no schedules" — that would un-schedule every
  suite it names without telling anyone.

The two halves are merged into one list — the shape `schedules --json`
prints and `--scheduled=<index>` indexes into. On a duplicate target the
file wins. Every merged entry carries its `source` (`config` or `file`); the
viewer edits only the file half and shows config entries read-only.

The cron expression is opaque to `@unotest/web`: it is validated as a
non-empty string and interpreted by whoever executes it, in that executor's
timezone (a box container is UTC unless its `TZ` says otherwise).

## Reading them back

```sh
npx @unotest/web schedules            # human list
npx @unotest/web schedules --json     # the merged list, as an executor reads it
npx @unotest/web schedules --check    # a gate for a pre-push hook or CI
```

The human list prints one line per entry — cron, target, and in brackets
`test` for a scenario entry, `env <name>`, `prepare \`…\``, `schedules.yaml`
for file entries — and a preview of the next tick on this machine's clock
(`next …`). An expression outside the previewable cron subset is marked
`not previewable — an executor may drop it`; `--check` turns that mark into
exit `1` (otherwise `schedules: N declared, all previewable.` and exit `0`).
An unreadable `schedules.yaml` exits `1`; mixing `--json` with `--check`, or
an unknown flag, exits `2`. With nothing declared the command prints how to
declare one.

## Running a schedule now

An executor runs an entry the same way you can by hand:

```sh
npx @unotest/web collection smoke --scheduled
npx @unotest/web e2e checkout/guest --scheduled
```

`--scheduled` runs the target *the way its entry does*: in the entry's `env`
(unless you set one yourself) and with its `prepare` command first, inside
the same run slot. A collection or test listed more than once needs
`--scheduled=<index>` — the zero-based position in `schedules`; without it
the command refuses to guess which env and preparation you meant. A failed
`prepare` exits **94**: the environment was not prepared and nothing was
tested, which is a different alert from a red suite (exit `1`).

## Schedules travel with the bundle

`npx @unotest/web bundle push` reads the effective schedules and stamps
their fingerprint into the bundle manifest, so the box that deploys the
bundle can compare what its own reading of the config yields against what
the pusher's machine saw. A registry the pusher's machine cannot read is
one the box cannot read either: the push is refused (`✗ error … → fix the
schedules registry — the box reads it to know what runs on its own`) while a
human is still watching, instead of un-scheduling the suite by tonight.
