> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.ampdb.io/api/mcp` to find what you need.

# Triggers

By default, a [job](/managing-amp/jobs) runs once. A **trigger** runs it again and again: every so many seconds, or on
a cron schedule. Amp uses one to collect each deployment's garbage every hour, and you can give one to the
garbage collection jobs you create, the only kind that takes a trigger.

## Prerequisites

* Amp running, with its Admin API reachable, and `ampctl` installed, as in [Jobs](/managing-amp/jobs#prerequisites).
* For the examples: the [Quickstart](/get-started/quickstart)'s `acme/eth_mainnet@1.0.0`, and its manifest hash in
  `MANIFEST_HASH`:

  ```bash
  export MANIFEST_HASH=$(ampctl dataset inspect acme/eth_mainnet@1.0.0 --json | jq -r .manifest_hash)
  ```

## Kinds of triggers

| Trigger              | Runs the job                                                                  |
| -------------------- | ----------------------------------------------------------------------------- |
| `one_shot` (default) | Once, when it's created.                                                      |
| `interval`           | When it's created, then every `every_secs` seconds, give or take some jitter. |
| `cron`               | When it's created, then on the cron schedule, in a fixed time zone.           |

`interval` and `cron` jobs are **periodic**. Between runs, a periodic job is `COMPLETED`, or `FATAL` if its last run
failed for good, and its trigger schedules it again when the next run is due. It stops for good only when you stop it.

### Interval

An interval trigger runs the job every `every_secs` seconds, counted from when the job was created, so a run that takes
long doesn't push the next ones back. For example, a job created at 12\:00:00 with a 6-hour interval runs around 18:00,
00:00, 06:00, and so on. Creating the job again starts the count over. After the controller restarts, it counts from
when the job was first created.

Each run starts a random number of seconds late, up to the **jitter** window, so that jobs created together don't all
run at once. The jitter window is:

* `jitter_secs`, from 0 to 30 seconds, when you set it. `0` turns jitter off.
* Otherwise, a tenth of the interval, up to 30 seconds.

Either way, it's never more than half the interval. A 5-minute interval gets 30 seconds of jitter, and a 1-minute
interval 6 seconds. A 40-second interval with `jitter_secs` 25 gets 20 seconds.

### Cron

A cron trigger runs the job on a cron schedule, five fields separated by spaces:

```
┌───────────── minute (0–59)
│ ┌─────────── hour (0–23)
│ │ ┌───────── day of the month (1–31)
│ │ │ ┌─────── month (1–12, or JAN–DEC)
│ │ │ │ ┌───── day of the week (1–7, from Sunday, or SUN–SAT)
│ │ │ │ │
0 3 * * *
```

Each field takes `*` for every value, a value like `3`, a list like `1,15`, a range like `MON-FRI`, or a step like
`*/6`. For example, `0 3 * * *` runs every day at 3:00, `0 */6 * * *` every 6 hours, and `30 9 * * MON-FRI` at 9:30
on weekdays.

:::warning
Days of the week are numbered from 1, for Sunday, to 7, for Saturday, unlike `crontab`, where Sunday is 0. `0` is
rejected, and `1` means Sunday. Use names, like `MON`, to avoid the difference.
:::

A schedule may also start with a seconds field, and then end with a year field, like `0 0 3 * * * 2027`: six fields are
seconds and the usual five, and a year needs seconds before it.

The schedule is read in the trigger's time zone, a fixed offset from UTC like `+05:30`, UTC by default. Named time
zones, like `America/New_York`, aren't supported, so a schedule doesn't follow daylight saving time: to run at 3:00 in
New York all year, change the offset from `-05:00` to `-04:00` when the clocks change.

## When a run is due

When a periodic job's next run is due, what happens depends on its status:

| Status                                     | What happens                                                                                                                                 |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `COMPLETED`, `FATAL`, or `CANCELLED`       | The job is scheduled again.                                                                                                                  |
| `ERROR`                                    | The job was waiting for a [retry](/managing-amp/jobs/retries). The retry is dropped, the job moves to `CANCELLED`, and it's scheduled again. |
| `SCHEDULED` or `RUNNING`                   | The previous run hasn't finished, so this one is skipped. The next one is still due on time.                                                 |
| `STOP_REQUESTED`, `STOPPING`, or `STOPPED` | The trigger ends.                                                                                                                            |

So runs never overlap: a run that takes longer than the interval makes the job skip the runs due in the meantime.

The controller keeps track of when runs are due. While it's down, no run starts. When it starts again, it schedules
each periodic job's next run from then on: the runs it missed are skipped, not made up.

## Triggers and retries

A periodic job keeps its [retry strategy](/managing-amp/jobs/retries) across runs. Its failures count until they leave
the strategy's window, not until its next run. So a job with a `bounded` strategy that fails on every run eventually
moves to `FATAL`. Its trigger still schedules it again, but its earlier failures are still in the window, so its next
failure moves it straight back to `FATAL`, until they leave it.

For a job that may fail on any run and should keep trying, give it a window shorter than its interval, a higher
`max_attempts`, or the default `unless_stopped` strategy.

## Create a periodic job

[`ampctl job create gc`](/managing-amp/jobs#collect-garbage) takes these options:

| Option                                    | Type                              | Default                           | Description                                                                                                              |
| ----------------------------------------- | --------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `--trigger <TRIGGER>`                     | `one_shot`, `interval`, or `cron` | `one_shot`                        | The trigger, like `--trigger interval`. The other options need it.                                                       |
| `--trigger-interval <TRIGGER_INTERVAL>`   | integer, at least 1               | none                              | How often the job runs, in seconds, like `--trigger-interval 21600`. Required with `interval`, and only allowed with it. |
| `--trigger-jitter <TRIGGER_JITTER>`       | integer, 0 to 30                  | a tenth of the interval, up to 30 | The jitter window, in seconds, like `--trigger-jitter 10`. `0` turns jitter off. Only allowed with `interval`.           |
| `--trigger-cron <TRIGGER_CRON>`           | cron schedule                     | none                              | The schedule, like `--trigger-cron '0 3 * * *'`. Required with `cron`, and only allowed with it.                         |
| `--trigger-time-zone <TRIGGER_TIME_ZONE>` | `+HH:MM` or `-HH:MM`              | `+00:00`                          | The time zone the schedule is read in, like `--trigger-time-zone +05:30`. Only allowed with `cron`.                      |

`ampctl` checks the options before it sends anything. An option the trigger doesn't allow fails with a message that
names it, like `--trigger-cron is only valid when --trigger=cron`.

For example, a deployment's own garbage collection job stops with the deployment (see
[Stop a deployment](/managing-amp/datasets#stop-a-deployment)). To keep collecting the garbage of a revision you
stopped, every 6 hours:

:::terminal
```bash
ampctl job create gc acme eth_mainnet "$MANIFEST_HASH" --trigger interval --trigger-interval 21600
```

```
✓ Job 9 created
```
:::

This schedules the deployment's garbage collection job again (job 3), since it has the same idempotency key, with the
new trigger. It works the same while the deployment runs: the job takes the new trigger instead of its hourly one, and
leaves the deployment, so stopping the deployment no longer stops it. And when a deployment has reached its end block,
`ampctl dataset stop` doesn't stop its garbage collection job, which keeps running every hour: stop it with
[`ampctl job stop`](/managing-amp/jobs/triggers#stop-a-periodic-job). Every day at 3:00 UTC instead:

```bash
ampctl job create gc acme eth_mainnet "$MANIFEST_HASH" --trigger cron --trigger-cron '0 3 * * *'
```

Every weekday at 3:00 in India (UTC+05:30):

```bash
ampctl job create gc acme eth_mainnet "$MANIFEST_HASH" \
  --trigger cron --trigger-cron '0 3 * * MON-FRI' --trigger-time-zone +05:30
```

### With the Admin API

A `gc` job request to [`POST /jobs`](/managing-amp/admin-api/jobs#create-a-job) takes a `trigger` object:

```bash
curl -X POST http://localhost:1610/jobs \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg hash "$MANIFEST_HASH" '{
    kind: "gc",
    dataset_namespace: "acme",
    dataset_name: "eth_mainnet",
    manifest_hash: $hash,
    trigger: {type: "interval", every_secs: 21600, jitter_secs: 10}
  }')"
```

| Field         | Type                                    | Default                           | Description                                          |
| ------------- | --------------------------------------- | --------------------------------- | ---------------------------------------------------- |
| `type`        | `"one_shot"`, `"interval"`, or `"cron"` | required                          | The trigger.                                         |
| `every_secs`  | integer, at least 1                     | required with `interval`          | How often the job runs, in seconds. `interval` only. |
| `jitter_secs` | integer, 0 to 30                        | a tenth of the interval, up to 30 | The jitter window, in seconds. `interval` only.      |
| `schedule`    | string                                  | required with `cron`              | The cron schedule. `cron` only.                      |
| `time_zone`   | string, `+HH:MM` or `-HH:MM`            | `"+00:00"`                        | The time zone the schedule is read in. `cron` only.  |

The other shapes:

```json
{ "trigger": { "type": "one_shot" } }
```

```json
{ "trigger": { "type": "cron", "schedule": "0 3 * * MON-FRI", "time_zone": "+05:30" } }
```

A trigger the API can't read, like a schedule with four fields or a time zone like `+5:30`, fails with
`400 Bad Request` and `INVALID_BODY`.

## Stop a periodic job

Stopping a periodic job ends its trigger, whether it's running or between runs:

```bash
ampctl job stop 3
```

The job moves to `STOPPED` and doesn't run again. To start it again, create it again. See
[Stop a job](/managing-amp/jobs#stop-a-job).
