---
name: schedules
description: Use when configuring a Windmill **schedule** — cron-based or interval trigger that runs a script/flow on a timer. Triggers on "schedule a script", "run every X minutes", "cron in windmill", `.schedule.yaml`, "schedule churns on every push", "perpetual ~ modify diff", "email field reverts", "cron_version v2", "ws_error_handler_muted", "Invalid range for Days of Week", "schedule deployer-stamped". Covers cron syntax (6-field, name-based DOW ranges), timezone, payload binding, enabled/disabled state, the three churn-preventing fields (cron_version + ws_error_handler_muted + email), deployer-stamped `email`/`permissioned_as`, multi-segment underscore filenames. NOT for: webhook/event triggers (use triggers skill).
---

# Windmill Schedules

Schedules run scripts and flows automatically on a cron schedule.

## File Naming

Schedule files use the pattern: `{path}.schedule.yaml`

Example: `f/folder/daily_sync.schedule.yaml`

Note: The path is derived from the filename, not stored in the file content.

## Cron Expression Format

Windmill uses 6-field cron expressions (includes seconds):

```
 ┌───────────── second (0-59)
 │ ┌───────────── minute (0-59)
 │ │ ┌───────────── hour (0-23)
 │ │ │ ┌───────────── day of month (1-31)
 │ │ │ │ ┌───────────── month (1-12 or jan-dec)
 │ │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday, or sun-sat)
 │ │ │ │ │ │
 * * * * * *
```

**Common Examples:**
- `0 0 0 * * *` - Daily at midnight
- `0 0 12 * * *` - Daily at noon
- `0 */5 * * * *` - Every 5 minutes
- `0 0 9 * * 1-5` - Weekdays at 9 AM
- `0 0 0 1 * *` - First day of each month

## CLI Commands

**Hallow ban:** `wmill sync push` and `wmill sync pull` are banned in this workspace. They delete server state not in local files and clobber secret variables. Mirror schedule changes to the server via the MCP `windmill` tools or the Windmill UI — never `wmill sync`.

After writing, tell the user they can run these read-only commands (do NOT run them yourself):

```bash
# List schedules
wmill schedule
```


## Schedule (`*.schedule.yaml`)

Must be a YAML file that adheres to the following schema:

```yaml
type: object
properties:
  schedule:
    type: string
    description: Cron expression with 6 fields (seconds, minutes, hours, day of month,
      month, day of week). Example '0 0 12 * * *' for daily at noon
  timezone:
    type: string
    description: IANA timezone for the schedule (e.g., 'UTC', 'Europe/Paris', 'America/New_York')
  enabled:
    type: boolean
    description: Whether the schedule is currently active and will trigger jobs
  script_path:
    type: string
    description: Path to the script or flow to execute when triggered
  is_flow:
    type: boolean
    description: True if script_path points to a flow, false if it points to a script
  args:
    type: object
    description: The arguments to pass to the script or flow
  permissioned_as:
    type: string
    description: The user or group this schedule runs as (e.g., 'u/sandbox' for admin elevation, or 'g/mygroup')
  on_failure:
    type: string
    description: Path to a script or flow to run when the scheduled job fails
  on_failure_times:
    type: number
    description: Number of consecutive failures before the on_failure handler is triggered
      (default 1)
  on_failure_exact:
    type: boolean
    description: If true, trigger on_failure handler only on exactly N failures, not
      on every failure after N
  on_failure_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  on_recovery:
    type: string
    description: Path to a script or flow to run when the schedule recovers after
      failures
  on_recovery_times:
    type: number
    description: Number of consecutive successes before the on_recovery handler is
      triggered (default 1)
  on_recovery_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  on_success:
    type: string
    description: Path to a script or flow to run after each successful execution
  on_success_extra_args:
    type: object
    description: The arguments to pass to the script or flow
  ws_error_handler_muted:
    type: boolean
    description: If true, the workspace-level error handler will not be triggered
      for this schedule's failures
  retry:
    type: object
    properties:
      constant:
        type: object
        description: Retry with constant delay between attempts
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          seconds:
            type: integer
            description: Seconds to wait between retries
      exponential:
        type: object
        description: Retry with exponential backoff (delay doubles each time)
        properties:
          attempts:
            type: integer
            description: Number of retry attempts
          multiplier:
            type: integer
            description: Multiplier for exponential backoff
          seconds:
            type: integer
            minimum: 1
            description: Initial delay in seconds
          random_factor:
            type: integer
            minimum: 0
            maximum: 100
            description: Random jitter percentage (0-100) to avoid thundering herd
      retry_if:
        $ref: '#/components/schemas/RetryIf'
    description: Retry configuration for failed module executions
  summary:
    type: string
    description: Short summary describing the purpose of this schedule
  description:
    type: string
    description: Detailed description of what this schedule does
  no_flow_overlap:
    type: boolean
    description: If true, skip this schedule's execution if the previous run is still
      in progress (prevents concurrent runs)
  tag:
    type: string
    description: Worker tag to route jobs to specific worker groups
  paused_until:
    type: string
    description: ISO 8601 datetime until which the schedule is paused. Schedule resumes
      automatically after this time
  cron_version:
    type: string
    description: Cron parser version. Use 'v2' for extended syntax with additional
      features
  dynamic_skip:
    type: string
    description: Path to a script that validates scheduled datetimes. Receives scheduled_for
      datetime and returns boolean to skip (true) or run (false)
  labels:
    type: array
    items:
      type: string
required:
- schedule
- script_path
- timezone
- is_flow
- enabled
- permissioned_as
```

## Hallow gotchas (schedules)

### Multi-segment filenames use underscore, not dot

A schedule whose path is `f/foo/bar_baz_v2` MUST live on disk as `bar_baz_v2.schedule.yaml`. A dotted name like `bar_baz.v2.schedule.yaml` derives a different entity path that won't match the server. Result: `wmill sync` shows the live schedule as a `-` delete with no paired `+`, and a push would DESTROY the running schedule. Always join multi-segment stems with `_` underscore.

### Three fields the scaffold omits — add them or churn forever

The server stores three schedule fields beyond the scaffold's defaults. Omitting them makes every sync diff a perpetual `~` modify:

| Field | Required value | Notes |
|---|---|---|
| `cron_version` | `v2` | Server default. |
| `ws_error_handler_muted` | `false` | Server default; mute only on purpose. |
| `email` | **the pushing user's email** | See auto-stamp note below. |

### `email` is deployer-stamped on every push

The server FORCES `email` to the identity of whoever runs the push — the active `wmill workspace` token owner. Run `wmill workspace` to confirm whose identity will be stamped. Setting an arbitrary value (e.g. a shared service email) is reverted to the deployer on the next push, then local diverges and churns. Same auto-stamp behavior as trigger `permissioned_as`.

**If a different run-as principal is required, that user must do the push.** Don't try to set it via YAML.

**Swap principal without re-pushing:** `wmill schedule set-permissioned-as <path> <email>` rewrites the server-side principal directly (no YAML diff, no sync). Requires admin / `wm_deployers`. New principal must have read on the impl script's folder. See `cli-commands` SKILL.md → "Server-side principal swap".

### Cron parser rejects numeric DOW ranges

Windmill's cron parser rejects numeric day-of-week ranges like `0-4` (error: `"Invalid range for Days of Week: 0-4"`). Use day-name ranges instead: `0 0 15 * * SUN-THU` rather than `0 0 15 * * 0-4`. Single numeric DOW (`* * * * * 0`) and lists (`0,3,5`) are fine — only RANGES require names.