# Schedules

> Call a path on your project on a cron timer in UTC, pause and resume it, and see when it last ran and how it answered.

Source: https://docs.fortly.com/deploy/schedules

A schedule asks for a path on your project at the times a cron expression names. Each run is an ordinary `GET` request to your project's own address, so it reaches your app the same way a visitor's request does. Times are in UTC, the expression has five fields, and an expression Fortly cannot run is refused when you save it rather than silently never firing.

## Add a schedule

**Console**

### Open schedules
Open your project, go to **Deployments**, and choose the **Schedules** tab.

### Describe the job
Under **Add a schedule**, give it a **Name** (what it is for), **When** it runs as a cron expression such as `0 3 * * *`, and the **Path** to request, such as `/cron/digest`.

### Save it
The schedule appears under **On this project**, with when it runs next.

**API**

```bash
curl -X POST https://api.fortly.com/projects/my-app-ada/schedules \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Nightly digest", "spec": "0 3 * * *", "path": "/cron/digest"}'
```

- `name` (string, required) — What the schedule is for. Cannot be empty.
- `spec` (string, required) — Five cron fields, in UTC. See [The cron format](#the-cron-format).
- `path` (string, required) — The path to request on your project. Must start with `/`, and may include a query string.

The reply is `201` with the schedule, including its `id` and `"enabled": true`. Use the project's full API name, such as `my-app-ada`. The full reference is in [Schedules API](https://docs.fortly.com/api/schedules).

## The cron format

Five fields separated by spaces, always evaluated in UTC:

```text
┌───────── minute        0-59
│ ┌─────── hour          0-23
│ │ ┌───── day of month  1-31
│ │ │ ┌─── month         1-12
│ │ │ │ ┌─ day of week   0-7 (0 and 7 are both Sunday)
│ │ │ │ │
0 3 * * *
```

Each field accepts:

| Syntax | Meaning | Example |
| --- | --- | --- |
| `*` | Every value | `*` in the hour field: every hour |
| A number | That value | `30` in the minute field |
| `a,b` | A list | `1,15` in the day field: the 1st and the 15th |
| `a-b` | A range, inclusive | `1-5` in the weekday field: Monday to Friday |
| `*/n` | Every nth value | `*/15` in the minute field: 0, 15, 30, 45 |
| `a-b/n` | Every nth value within a range | `9-17/2` in the hour field: 9, 11, 13, 15, 17 |

When both the day-of-month and the day-of-week fields are restricted, a day that matches **either** one runs. `0 9 1 * 1` runs at 09:00 on the 1st of every month and on every Monday. This is how cron has always behaved.

### What is refused

Fortly accepts numbers only. These are refused when you save, with a message that says which field is wrong:

| You wrote | Refused with |
| --- | --- |
| `@daily`, `@hourly` | `a schedule has five fields — minute hour day month weekday — and this has 1` |
| Six fields, with seconds | `a schedule has five fields — minute hour day month weekday — and this has 6` |
| `0 9 * * MON` | `weekday: MON is not a number` |
| `0 9 * JAN *` | `month: JAN is not a number` |
| `60 * * * *` | `minute: 60 is outside 0-59` |
| `*/0 * * * *` | `minute: a step of zero never comes round` |

`L`, `W` and `#` are not supported either.

An expression that can never come round, such as `0 0 31 2 *` (31 February), is accepted but never runs. Its `next_run_at` is `null`, which is how you spot one.

### Examples

| Expression | Runs |
| --- | --- |
| `*/5 * * * *` | Every 5 minutes |
| `0 * * * *` | At the start of every hour |
| `0 3 * * *` | Every day at 03:00 UTC |
| `30 8 * * 1-5` | Weekdays at 08:30 UTC |
| `*/15 9-17 * * 1-5` | Every 15 minutes from 09:00 to 17:45 UTC, Monday to Friday |
| `0 0 * * 0` | Every Sunday at midnight UTC |
| `0 0 1 * *` | The first of every month at midnight UTC |
| `0 6 1 1,4,7,10 *` | 06:00 UTC on the first day of each quarter |

## How a run works

At the top of each minute, Fortly checks every enabled schedule and runs the ones whose expression matches that minute.

- **The request** is a `GET` to the schedule's path on your project's own address. It goes through the same front door as visitor traffic, so it appears in your project's [metrics](https://docs.fortly.com/deploy/logs#metrics) like any other request.
- **Success** is any `2xx` or `3xx` answer. Anything else is recorded as an error: `the project answered 500`.
- **Time limit**: a run that has not answered within five minutes is abandoned and recorded as `the job did not finish within five minutes`.
- **The result** is kept on the schedule: `last_run_at`, `last_status` (the HTTP status), `last_ms` (how long it took) and `last_error`.

> **Warning:**
>
> A scheduled run carries no Fortly credential or header that marks it as scheduled. Anyone who knows the path can request it too. Make the job safe to run more than once, and check your own secret in the path's query string or handler if running it must be restricted.

### Missed runs are not replayed

If a run's minute passes while Fortly cannot run it — during maintenance on the machine that runs schedules, for example — that run is skipped, not made up later. A five-minute schedule that missed an hour does not fire twelve times when it can run again. The schedule's `last_run_at` shows when it last actually ran.

## Pause, resume and remove

**Console**

Each schedule under **On this project** has a **Pause** button, which becomes **Resume** while it is paused. A paused schedule shows **Paused** where its next run would be.

**API**

```bash
# Pause
curl -X PATCH https://api.fortly.com/projects/my-app-ada/schedules/SCHEDULE_ID \
  -H "Authorization: Bearer $FORTLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

# Remove
curl -X DELETE https://api.fortly.com/projects/my-app-ada/schedules/SCHEDULE_ID \
  -H "Authorization: Bearer $FORTLY_TOKEN"
```

Send `{"enabled": true}` to resume. Both answer `204`, or `404` with `no such schedule`.

A paused schedule does not run and its `next_run_at` is `null`. Resuming it does not run the times it missed while paused. To change a schedule's expression or path, remove it and add it again.

## List schedules

```bash
curl https://api.fortly.com/projects/my-app-ada/schedules \
  -H "Authorization: Bearer $FORTLY_TOKEN"
```

Each schedule in `schedules` comes with `next_run_at`, the next time it will run in seconds since the epoch, worked out when you ask.

- `GET /projects/{name}/schedules` — A project's schedules, with their next run
- `POST /projects/{name}/schedules` — Add one — name, spec, path
- `PATCH /projects/{name}/schedules/{id}` — Pause or resume
- `DELETE /projects/{name}/schedules/{id}` — Remove it
- `GET /schedules` — Every schedule across the projects you can reach

## Related

- [Schedules API](https://docs.fortly.com/api/schedules): Every schedules route, its parameters and its responses.
- [Logs and metrics](https://docs.fortly.com/deploy/logs): See scheduled requests in your project's traffic.
- [Environment and secrets](https://docs.fortly.com/deploy/environment): Keep the secret your job checks out of your code.
