# Scheduler Config

Pass a command-keyed record to `scheduler()`. Each key identifies the command to dispatch; one command id can have one schedule.

```typescript
scheduler({
  'content.fetch': '5m',
  'content.clear': { cron: '0 3 * * *' },
});
```

> [!NOTE]
> Multiple independent schedules for the same command id are not supported. The record shape allows one schedule per key.

For a complete content-refresh setup, see the [Live Content Refresh recipe](/recipes/live-content-refresh/).

## Schedule shorthand

A bare string is either a duration or a cron expression:

- A string with no whitespace is a duration, such as `'500ms'`, `'30s'`, `'5m'`, or `'2h'`.
- A string with whitespace is a cron expression and must contain exactly five whitespace-separated fields.

Durations may also be raw millisecond numbers in the object form.

## Per-job options

Use an object when a job needs options beyond its schedule:

```typescript
scheduler({
  'content.fetch': {
    interval: '5m',
    jitter: '30s',
    retry: {
      forever: true,
      backoff: { initial: '10s', max: '2m', factor: 2 },
    },
    command: { type: 'content.fetch' },
    runOnStart: false,
    enabled: true,
  },
});
```

The schema reference covers each record value. Input defaults do not include values filled by transforms or the scheduler at runtime; those behaviors are explained below. Requiredness is structural, so read the conditional rules for `interval`, `cron`, and retry branches. See [configuration reference conventions](/reference/cli/config-loading/#configuration-reference-conventions).

**Input type:** `ScheduleSpec`

**Required:** No

**Input default:** —

### `interval`

**Input type:** `string | number`

**Required:** No

**Input default:** —

A duration between interval runs. The next interval is computed when the previous dispatch completes, so interval jobs cannot overlap themselves.

### `cron`

**Input type:** `string`

**Required:** No

**Input default:** —

A five-field cron expression. Cron schedules stay anchored to wall-clock occurrences. The next occurrence is armed before the current dispatch completes; a command already in progress is skipped rather than queued.

Exactly one of `interval` and `cron` is required.

### `jitter`

**Input type:** `boolean | string | number`

**Required:** No

**Input default:** `true`

Adds a fresh, positive random delay to every scheduled run and retry, spreading a fleet of kiosks out so they don’t all fire at once.

The `true` default resolves differently by schedule type:

- **Interval jobs** (and retry backoff): up to 10% of the delay being scheduled.
- **Cron jobs:** up to 60 seconds. A cron gap can be many hours, so a percentage of it would drag the run far off its wall-clock time; the fixed 60-second cap desyncs a fleet without shifting the occurrence meaningfully. For example, a nightly `'0 3 * * *'` job still fires within a minute of 3am.

Supply a duration to set a fixed maximum jitter for either schedule type, or `false` to disable jitter.

### `retry`

**Input type:** `object`

**Required:** No

**Input default:** —

Failed dispatches retry before the normal schedule resumes. A successful dispatch resets the failure count and backoff. Choose one of two policies:

- **Forever:** omit `retry`, use `{}`, or set `forever: true`. Only this policy accepts `backoff` overrides; it does not accept `maxAttempts`.
- **Limited:** set `forever: false` and supply `maxAttempts`. This policy uses the default backoff curve and does not accept `backoff` overrides.

```typescript
scheduler({
  'content.fetch': {
    interval: '5m',
    retry: { forever: true, backoff: { initial: '10s', max: '2m', factor: 2 } },
  },
});
```

To stop after a fixed number of failed attempts instead:

```typescript
scheduler({
  'content.fetch': {
    interval: '5m',
    retry: { forever: false, maxAttempts: 3 },
  },
});
```

A command rejected because it is already running is an overlap skip, not a failure. It does not consume an attempt or begin backoff.

#### `forever`

**Input type:** `false | true`

**Required:** No

**Input default:** —

Selects the retry policy. Omitting this field selects forever retries; `false` selects limited retries and requires `maxAttempts`. The field metadata describes all accepted inputs together; conditional requirements are specified in each policy above.

#### `maxAttempts` (limited policy)

**Input type:** `number`

**Required:** No

**Input default:** —

Required when `forever: false`, even though it is not required across the combined input schema. Must be a positive integer. Not accepted with `forever: true` or an omitted `forever` field; it cannot be combined with `backoff`.

#### `backoff` (forever policy)

**Input type:** `object`

**Required:** No

**Input default:** —

Available only with the forever policy. Omitted fields use the runtime defaults described in the table, not schema input defaults. `initial` and `max` accept duration shorthand or milliseconds; `factor` must be positive.

```typescript
scheduler({
  'content.fetch': {
    interval: '5m',
    retry: { backoff: { initial: '10s' } },
  },
});
```

| Field     | Input type         | Required | Input default | Meaning                                               |
| --------- | ------------------ | -------- | ------------- | ----------------------------------------------------- |
| `initial` | `string \| number` | No       | —             | Initial retry delay; omitted values use 15 seconds.   |
| `max`     | `string \| number` | No       | —             | Maximum retry delay; omitted values use five minutes. |
| `factor`  | `number`           | No       | —             | Positive backoff multiplier; omitted values use 2.    |

### `command`

**Input type:** `BaseCommand`

**Required:** No

**Input default:** —

Overrides the command dispatched for this job. This is useful when the command needs a payload. When omitted, the scheduler dispatches `{ type: <record key> }`.

```typescript
scheduler({
  'content.fetch': {
    interval: '5m',
    command: { type: 'content.fetch', force: true },
  },
});
```

### `runOnStart`

**Input type:** `boolean`

**Required:** No

**Input default:** `false`

Dispatches the job immediately when the scheduler starts. When `false`, the first dispatch occurs after the scheduled delay.

### `enabled`

**Input type:** `boolean`

**Required:** No

**Input default:** `true`

When `false`, the job is never scheduled or dispatched and does not appear in the Scheduler status section. A disabled job is invisible to the runtime commands too: `scheduler.pause`, `scheduler.resume`, and `scheduler.trigger` treat its id as unknown, and a broadcast `scheduler.pause`/`scheduler.resume` (no `job` given) skips it entirely.

## Limitations

Schedules and retry state are in memory only. On restart, each job receives a fresh schedule from the current time, and cron occurrences missed while Launchpad was stopped are skipped.
