Skip to content

Scheduling

Any test can run on a schedule: every hour, every weekday morning, every night. Scheduled runs catch problems that happen without a deploy, such as an expired certificate, a third-party script that broke, or a slow page after a data import. They behave like any other run: results, retries, flaky marking and alerts all apply.

  1. Open the test and its Settings tab (or the Options step when you create a test).

  2. Under Schedule, pick a preset:

    Preset Cron expression
    Every hour 0 * * * *
    Every 6 hours 0 */6 * * *
    Daily · 09:00 UTC 0 9 * * *
    Weekdays · 09:00 UTC 0 9 * * 1-5
    Weekly · Mon 09:00 UTC 0 9 * * 1
  3. Click Save changes. The test’s overview shows its schedule.

The Schedule field set to Weekdays · 09 UTC

To remove a schedule, choose No schedule. To pause a test without losing its schedule, switch off Active under Settings → Status; paused tests are skipped by schedules, Run all and CI.

Any standard five-field cron expression works (minute, hour, day of month, month, day of week). All times are UTC. Set one through the API; the app then shows it in the menu as a custom entry, for example Custom · 30 2 * * *.

Expression Runs
*/15 * * * * every 15 minutes
30 2 * * * every night at 02:30 UTC
0 8,12,16 * * 1-5 at 08:00, 12:00 and 16:00 UTC on weekdays
0 6 1 * * at 06:00 UTC on the first day of every month
Terminal window
curl -s -X PUT "$PW/api/test-cases/TEST_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"schedule_cron": "30 2 * * *"}'

The response includes next_run, the next time the schedule fires in UTC. If next_run is null, the expression is invalid; check it before relying on it, because an invalid expression is silently skipped. To clear a schedule through the API, send "schedule_cron": "". Sending null leaves the schedule unchanged.

Schedules are run by the scheduler service (python -m app.services.scheduler), a separate process in the Compose stack. Once a minute it looks at every active test that has a schedule:

  1. It works out the most recent time the cron expression fired, at or before now.
  2. If the test has had no run of any kind since that time, it queues one.

This is stateless, so restarts are safe:

  • A missed slot fires once. If the scheduler was down over several fire times, the test runs once when it comes back, not once per missed slot.
  • Any run counts. If someone ran the test by hand, from CI or with Run all after the last fire time, the scheduler treats that slot as done and waits for the next one.
  • A busy queue doesn’t double-fire. The run is created when it is queued, so a run still waiting for a worker counts as well.

Scheduled runs:

Setting Value
Trigger shown on the run schedule
Base URL the project URL. Environments selected in the top bar don’t apply.
Browser and device Chromium · Desktop
Retries, trials, assertions, visual checks as configured on the test
Alerts and email as configured for the project

On the same one-minute tick, the scheduler looks for runs that started (or were queued) long ago and never finished, for example because a worker restarted during a deploy. A run still pending or running after its time limit plus a 60-second grace period is marked error with:

Run did not complete — its worker stopped or it exceeded the time limit (orphaned job reaped). Re-run to try again.

The time limit is 300 seconds per trial, so a single-trial test is cleaned up after 6 minutes and a test with 3 trials after 16 minutes. The live view of that run closes at the same time.

  • Run one scheduler. The due check is stateless but not locked: two scheduler processes can queue the same slot twice. Scale workers, not schedulers.
  • It needs the database and Redis, like the backend. It doesn’t run tests itself; workers do.
  • Same SECRET_KEY as the backend and worker, as for every service.
  • Logs. It logs each run it queues (scheduled run for test_case=…) and each run it cleans up (reaped orphaned run …). Check them with docker compose logs scheduler.