# Scheduling

Run a plan on a cron schedule, with overlap protection, retries and dependencies. Approval gates apply exactly as they do to a manual run.

Source: https://opafra.com/docs/running/scheduling

---

A scheduled task runs a plan on a cron schedule. It carries its own execution mode,
variables and environment, so the schedule is a complete description of a recurring run
rather than a trigger that needs context from elsewhere.

## Before you begin

You need the **Operator** role or above, and a plan that already runs correctly when
started by hand.

## 1. Create the schedule

Give it a name, the plan to run, a cron expression, and a timezone.

```text title="cron"
0 2 * * *      every day at 02:00
0 */4 * * *    every four hours
30 3 * * 0     Sundays at 03:30
```

The timezone is stored with the schedule and defaults to UTC. Setting it to the timezone
your team actually works in is usually right, and it means a schedule does not drift by an
hour twice a year relative to your maintenance window.

A schedule can be disabled without being deleted, which is what you want during an
incident or a freeze.

## 2. Set overlap protection

By default a schedule will not start if its previous run is still going. The fire is
skipped and logged rather than queued, because a backup job that takes longer than its
interval should not accumulate copies of itself.

`max_execution_time` bounds how long the previous run is allowed to hold that lock, so a
genuinely stuck run does not suppress the schedule forever.

Overlap can be allowed where concurrent runs are genuinely safe. Most of the time they are
not, which is why the default is off.

## Retries

A failed scheduled run can retry a set number of times, with a delay between attempts.

Retries are worth it for failures that are plausibly transient: a host that was rebooting,
a repository that was briefly unreachable. They are not worth it for a plan that fails
because it is wrong, where three attempts produce three identical failures and a noisier
log.

## Dependencies

A schedule can declare that it runs only after other tasks, with three modes:

| Mode | Runs when |
|---|---|
| **All success** | Every named task succeeded |
| **Any success** | At least one succeeded |
| **All complete** | All finished, whether or not they succeeded |

`All complete` is the one to reach for when a cleanup task should run regardless of what
happened upstream. `All success` is the default and the right choice for a genuine
pipeline.

## Approval gates still apply

> **Important**
>
> A scheduled run goes through exactly the same path as a manual one. If it targets an
> environment that requires approval, it pauses and waits for a person, at 02:00 or
> whenever it fired.

This is worth planning around rather than discovering. Two workable shapes:

- **Schedule against environments that do not require approval**, and keep gated
  environments for changes a person is initiating anyway.
- **Accept the pause**, and treat the approvals inbox as the queue it becomes. Reasonable
  when the schedule is infrequent and someone is expected to review it.

What does not work is scheduling a gated plan overnight and expecting it to have run by
morning. It will be waiting.

## Dry runs on a schedule

A schedule can run in dry-run mode, which changes nothing and reports what would happen.

This is a genuinely useful pattern: a nightly dry run of your production plans is drift
detection. If a plan that reported `no change` for a month starts reporting `would
change`, something moved on those hosts that nobody recorded.

## History

Each fire produces a run with its own record, kept even if the schedule is later deleted,
so the history of what ran does not disappear with the thing that scheduled it.

## Verify it worked

Open **Schedules**. The new schedule is listed as enabled, with the next fire time
resolved in the timezone you set:

```text title="schedules"
nightly-patch    0 2 * * *    Europe/Paris    enabled    next: 2026-09-09 02:00
```

Do not wait for the first fire to find out whether it works. Start the plan by hand once
and confirm it completes, then check back after the first scheduled run: each fire
produces its own run in the run history, with the schedule named as what started it.

## Next steps

- [Approval gates](/docs/running/approval-gates) for what a gated schedule does
- [Running a plan](/docs/running/executing) for modes and failure behaviour
