> ## Documentation Index
> Fetch the complete documentation index at: https://developers.zite.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedules

> Run workflows on a recurring cadence or once at a future time — inline on createEndpoint, or at runtime with ZiteSchedules.

Any [workflow](/framework/workflows) can run on a schedule instead of (or in addition to) being called
from an app. Scheduled runs execute in the background with **no user** — `context.user` is `null`, and the
context carries `scheduledAt`.

## Inline schedule

Add a `schedule` to `createEndpoint` to run it on a fixed cadence:

```typescript theme={null}
// apps/helpdesk/src/api/dailyDigest.ts
import { z } from 'zod';
import { createEndpoint } from 'zitejs/backend';

export default createEndpoint({
  description: 'Send the daily digest',
  inputSchema: z.object({}),
  outputSchema: z.object({ sent: z.number() }),
  schedule: {
    scheduleType: 'recurring',
    schedule: { frequency: 'daily', interval: 1, times: ['09:00'] },
    timezone: 'America/New_York',
  },
  execute: async () => {
    // …build and send the digest
    return { sent: 1 };
  },
});
```

### The schedule shape

A schedule is either `recurring` or `oneTime`:

```typescript theme={null}
type ZiteSchedule =
  | {
      scheduleType: 'recurring';
      schedule:
        | { frequency: 'minutely'; interval: number; activeWindow?: ActiveWindow }
        | { frequency: 'hourly';   interval: number; minute?: number; activeWindow?: ActiveWindow }
        | { frequency: 'daily';    interval: number; times: string[] }            // "HH:MM"
        | { frequency: 'weekly';   interval: number; daysOfWeek: DayOfWeek[]; times: string[] }
        | { frequency: 'monthly';  interval: 1; monthlyDay: MonthlyDay; times: string[] };
      endPolicy?: { type: 'never' } | { type: 'onDate'; endAt: string } | { type: 'afterOccurrences'; occurrences: number };
      overlapPolicy?: 'skip' | 'allow';   // default 'skip'
      timezone: string;                    // IANA, e.g. "America/New_York"
      paused?: boolean;
    }
  | { scheduleType: 'oneTime'; fireAt: string; timezone: string; paused?: boolean };
```

Each `frequency` takes a different set of fields:

| `frequency` | Required                                        | Optional                 |
| ----------- | ----------------------------------------------- | ------------------------ |
| `minutely`  | `interval`                                      | `activeWindow`           |
| `hourly`    | `interval`                                      | `minute`, `activeWindow` |
| `daily`     | `interval`, `times`                             | —                        |
| `weekly`    | `interval`, `daysOfWeek`, `times`               | —                        |
| `monthly`   | `interval` (must be `1`), `monthlyDay`, `times` | —                        |

<Note>
  `monthly` is the one frequency that pins `interval` to `1`. Every other frequency accepts any number —
  `{ frequency: 'hourly', interval: 6 }` runs every six hours.
</Note>

And the shared fields:

| Field           | Type                                    | Notes                                                                                              |
| --------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `times`         | `string[]`                              | `"HH:MM"` strings, in the schedule's `timezone`                                                    |
| `timezone`      | `string`                                | Required. IANA name, e.g. `"America/New_York"`                                                     |
| `activeWindow`  | `ActiveWindow`                          | Minutely and hourly only — restricts firing to certain hours/days                                  |
| `monthlyDay`    | `MonthlyDay`                            | Monthly only — which day to fire on. See below                                                     |
| `endPolicy`     | `never` · `onDate` · `afterOccurrences` | When the recurrence stops. Omit to run indefinitely                                                |
| `overlapPolicy` | `'skip'` · `'allow'`                    | Defaults to `'skip'`, which prevents a new run from starting while the previous one is still going |
| `paused`        | `boolean`                               | Suspends the schedule without deleting it                                                          |

`monthlyDay` picks the day either by number or by weekday occurrence:

```typescript theme={null}
type MonthlyDay =
  | { type: 'dayOfMonth'; day: number | 'last' }              // 1–31, or the last day
  | { type: 'weekdayOccurrence';                              // e.g. the 2nd Tuesday
      weekday: DayOfWeek;
      occurrence: 1 | 2 | 3 | 4 | 'last' };
```

## Managing schedules at runtime

Import `ZiteSchedules` from `zitejs/schedules` to add or change schedules from inside your logic (for
example, scheduling a reminder when a record is created):

```typescript theme={null}
import { ZiteSchedules } from 'zitejs/schedules';

// Schedule a one-time reminder
const { id } = await ZiteSchedules.add({
  endpointId: 'sendReminder',
  schedule: { scheduleType: 'oneTime', fireAt: '2026-08-01T09:00:00Z', timezone: 'UTC' },
  inputs: { ticketId: 'rec_123' },
});

await ZiteSchedules.list({ endpointId: 'sendReminder' }); // { schedules: [...] }
await ZiteSchedules.update({ id, schedule: /* … */ });
await ZiteSchedules.remove({ id });
```

| Method   | Arguments                           | Returns             |
| -------- | ----------------------------------- | ------------------- |
| `add`    | `{ endpointId, schedule, inputs? }` | `{ id, cronJobId }` |
| `list`   | `{ endpointId? }`                   | `{ schedules }`     |
| `update` | `{ id, schedule?, inputs? }`        | `{ id, updated }`   |
| `remove` | `{ id }`                            | `{ id, deleted }`   |

<Note>
  Schedule `inputs` are limited to **32 KB**. Runtime schedule changes require the app to have been
  published at least once.
</Note>
