---
title: "Schedules"
description: "Start runs of a workflow on a cron timetable, with time zones, overlap and catch-up policies, and jitter."
---

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

# Schedules

A schedule starts runs of one workflow in one app on a cron timetable. A project can have up to
1,000 schedules.

### TypeScript

```ts
const s = await client.createSchedule({
  appId, workflow: "nightly-report", name: "nightly", cron: "0 2 * * *", timeZone: "Europe/Berlin", jitterSeconds: 300,
})
await client.updateSchedule(s.id, { cron: "30 2 * * *" }, { revision: s.revision })
await client.triggerSchedule(s.id, { idempotencyKey: "rerun-2026-10-10" })
await client.pauseSchedule(s.id)
await client.resumeSchedule(s.id)
await client.deleteSchedule(s.id)
```
### Go

```go
jitter := 300
s, err := c.CreateSchedule(ctx, workflows.ScheduleSpec{
	AppID: appID, Workflow: "nightly-report", Name: "nightly", Cron: "0 2 * * *",
	TimeZone: "Europe/Berlin", JitterSeconds: &jitter,
})
run, err := c.TriggerSchedule(ctx, s.ID, "rerun-2026-10-10")
```
### Python

```python
s = client.create_schedule(
    app_id=app_id, workflow="nightly-report", name="nightly", cron="0 2 * * *",
    time_zone="Europe/Berlin", jitter_seconds=300,
)
client.update_schedule(s["id"], revision=s["revision"], cron="30 2 * * *")
client.trigger_schedule(s["id"], idempotency_key="rerun-2026-10-10")
```

Or call [CreateSchedule](/workflows-api/CreateSchedule/). A schedule has:

| Field | Meaning |
| --- | --- |
| `appId`, `workflow` | The workflow to run. The app can't change later. |
| `name` | A name, 1 to 100 characters. Names don't have to be unique. |
| `cron` | When to run ([below](#cron-expressions)) |
| `timeZone` | An IANA time zone, such as `Europe/Berlin`. Default `UTC`. |
| `input` | The JSON input of every run, at most 1 MiB. Default none. |
| `overlap` | What to do when the previous run is still going: `skip` (default), `allow` or `bufferOne` ([below](#overlap)) |
| `catchupWindowSeconds` | How late a missed run may still start, 0 to 86,400. Default 3,600 ([below](#catch-up)) |
| `jitterSeconds` | Delay each run by a stable offset of 0 to this many seconds, at most 3,600. Default 0 ([below](#jitter)) |
| `paused` | Create it paused |

A schedule's view also shows its `nextScheduledTime`, `lastScheduledTime`, `activeRunId`, how
many runs it `missed` and `skipped`, and who paused it: `pausedBy` is `customer` (you paused it),
`system` (EZGH paused it while its organization's deletion is pending), or `null` while it runs.
Runs a schedule started are listed with [`scheduleId`](/workflows/runs).

Creating a schedule, changing anything but its name, resuming it and triggering it need
`workflows.runs.start` on its workflow, as well as the schedule action.

## Cron expressions

- **Five fields:** minute, hour, day of month, month, day of week, such as `0 2 * * *` (02:00
  every day) or `*/15 9-17 * * 1-5` (every 15 minutes during working hours on weekdays). Or one of
  `@hourly`, `@daily`, `@weekly`, `@monthly` and `@yearly`.
- **At most once a minute:** there's no seconds field.
- **Daylight saving time:** a local time that doesn't exist on a day (when clocks go forward) is
  skipped that day. A local time that happens twice (when clocks go back) runs once, the first
  time.

## The runs a schedule starts

- **Each run's ID is derived from the schedule and its scheduled time,** so a scheduled time can
  never start two runs.
- **The workflow sees which schedule started it:** `run.schedule` (`{ id, scheduledTime }`) in
  TypeScript, `ctx.Run().Schedule` in Go, and `ctx.run.schedule` in Python. `scheduledTime` is the
  cron time, whatever the jitter.
- Runs started by a schedule aren't recorded in [Trails](/trails).

## Overlap

When a scheduled time comes and the schedule's previous run hasn't ended:

| `overlap` | What happens |
| --- | --- |
| `skip` | No run starts; the time counts as skipped. The default. |
| `allow` | The run starts anyway. |
| `bufferOne` | One run starts when the previous one ends. Further times meanwhile are skipped. |

## Catch-up

After the schedule was paused or couldn't fire (an outage), only the **most recent** missed time
runs, and only if it's within `catchupWindowSeconds`. Older missed times count as missed and never
run, so a schedule never starts a burst of runs to catch up. `0` means never catch up.

**Resuming** a paused schedule fires again from now, with no catch-up for the time it was paused.
Pausing and resuming are idempotent.

## Jitter

Many schedules share the same times, such as `0 * * * *`. `jitterSeconds` delays each run by an
offset between 0 and that many seconds. The offset is derived from the schedule and the scheduled
time, so it's stable, and it never moves a run past the schedule's next time. The scheduled time
the workflow sees doesn't change. Use it for anything that doesn't need to start on the exact
second.

## Change, trigger and delete

- **Change** a schedule with [UpdateSchedule](/workflows-api/UpdateSchedule/): only the
  fields you send change. Every change increments its `revision`, which is its `ETag`. Send
  `If-Match: "<revision>"` (the SDKs take the revision) to change it only if nobody else did:
  otherwise it's `412 precondition_failed`.
- **Trigger** starts a run now with
  [TriggerSchedule](/workflows-api/TriggerSchedule/). It needs an `Idempotency-Key`, which
  names the run: a repeat with the same key answers the same run, replayed with its status (`201`)
  and `Idempotent-Replayed: true`. A trigger ignores the
  overlap policy and doesn't count as a scheduled run; its `scheduledTime` is when it was
  triggered.
- **Delete** stops the schedule at once; runs it already started continue. Deleting an app deletes
  its schedules.

Lists of schedules show changes about a second after they're made; getting a schedule by ID is
always current.

## Errors

| Status | Meaning |
| --- | --- |
| `400 invalid_request` | A field isn't valid: a cron expression that runs more than once a minute, an unknown time zone, an app that isn't in the project |
| `400 idempotency_key_required` | A trigger without an `Idempotency-Key` |
| `409 quota_exceeded` | The project has 1,000 schedules |
| `409 app_deleted` | Resuming or triggering a schedule whose app was deleted |
| `412 precondition_failed` | `If-Match` names an old revision |
| `413` | The input is over 1 MiB |

Source: https://docs.ezghcloud.com/workflows/schedules/index.mdx
