#23097·prefect

prefect deploy ignores explicit schedules: [], retaining an active schedule (3.8.5)

Author: coryosoCreated Sep 14, 2026Updated Sep 17, 2026
Labelsenhancement

Bug summary

On Prefect 3.8.5, changing an existing deployment's YAML from a cron schedule to an explicit schedules: [] and rerunning prefect --no-prompt deploy --all --prefect-file prefect.yaml succeeds but leaves the original schedule active.

I reproduced this against a fresh local Prefect server and temporary SQLite database, using the actual CLI and API without mocks. Both deploy commands exit successfully. The stored schedule ID is unchanged after the second deployment.

Expected: an explicit empty list removes the stored schedules (stored=0), while keeping the deployment available for manual runs. This report concerns an explicit [], not an omitted schedules field.

Actual: the original schedule remains (stored=1, active=[True], deployment paused=False). This can leave a job scheduled after its cron has been deliberately removed from version-controlled configuration.

Reproduction

Save the following as repro.py. It uses Prefect's test harness, so it needs no Cloud account, worker, or existing deployment. It creates a process work pool, deploys a single cron, redeploys with [], reads the stored schedules after each deployment, and checks the native clear command afterward.

bash
PREFECT_API_KEY='' PREFECT_API_URL='' PREFECT_SERVER_ANALYTICS_ENABLED=false \
  uv run --isolated --no-project --python 3.11 --with 'prefect==3.8.5' python repro.py
python
import os
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory

import yaml
from prefect.client.orchestration import get_client
from prefect.client.schemas.actions import WorkPoolCreate
from prefect.settings import PREFECT_API_URL
from prefect.testing.utilities import prefect_test_harness

with TemporaryDirectory() as directory, prefect_test_harness():
    root = Path(directory)
    (root / "flow.py").write_text(
        "from prefect import flow\n@flow\ndef hello():\n    pass\n"
    )
    config = {
        "name": "empty-schedules-repro",
        "build": [], "push": [], "pull": [],
        "deployments": [{
            "name": "repro",
            "entrypoint": "flow.py:hello",
            "work_pool": {"name": "repro-pool"},
        }],
    }
    env = {**os.environ, "PREFECT_API_URL": PREFECT_API_URL.value()}
    with get_client(sync_client=True) as client:
        client.create_work_pool(WorkPoolCreate(name="repro-pool", type="process"))
        subprocess.run([sys.executable, "-m", "prefect", "version"], env=env, check=True)
        schedule_ids = []
        for schedules in ([{"cron": "0 6 * * *", "timezone": "UTC"}], []):
            config["deployments"][0]["schedules"] = schedules
            (root / "prefect.yaml").write_text(yaml.safe_dump(config))
            subprocess.run(
                [sys.executable, "-m", "prefect", "--no-prompt", "deploy",
                 "--all", "--prefect-file", "prefect.yaml"],
                cwd=root, env=env, check=True,
            )
            deployment = client.read_deployment_by_name("hello/repro")
            stored = client.read_deployment_schedules(deployment.id)
            schedule_ids.append([schedule.id for schedule in stored])
            print(f"YAML schedules={schedules!r}: stored={len(stored)}, "
                  f"active={[s.active for s in stored]}, paused={deployment.paused}", flush=True)
        print(f"Same stored schedule after redeploy: {schedule_ids[0] == schedule_ids[1]}", flush=True)
        subprocess.run(
            [sys.executable, "-m", "prefect", "deployment", "schedule", "clear",
             "hello/repro", "--accept-yes"], env=env, check=True,
        )
        print(f"After native CLI clear: stored={len(client.read_deployment_schedules(deployment.id))}", flush=True)

Relevant output:

YAML schedules=[{'cron': '0 6 * * *', 'timezone': 'UTC'}]: stored=1, active=[True], paused=False
YAML schedules=[]: stored=1, active=[True], paused=False
Same stored schedule after redeploy: True
After native CLI clear: stored=0

There is no exception or failed deployment; the unexpected result is the retained active schedule.

Version info

Version:              3.8.5
API version:          0.8.4
Python version:       3.11.15
Git commit:           48617507
Built:                Thu, Sep 03, 2026 09:28 PM
OS/Arch:              darwin/arm64
Profile:              ephemeral
Server type:          server
Pydantic version:     2.13.5
Server:
  Database:           sqlite
  SQLite version:     3.53.1

Additional context

The YAML transition in the reproduction is:

yaml
# First deploy
schedules:
  - cron: "0 6 * * *"
    timezone: UTC

# Second deploy, same deployment name and entrypoint
schedules: []

A likely client-side cause is that the CLI passes deploy_config.get("schedules") to deployment.apply, but the existing-deployment branches of RunnerDeployment.aapply and apply only assign the supplied schedules under if schedules:. An explicit empty list is therefore skipped. This is a source-based diagnosis, not a tested upstream patch.

The working CLI workaround is:

bash
prefect deployment schedule clear "hello/repro" --accept-yes

That removes the schedules, but requires an extra operation outside applying the YAML. Pausing the deployment retains the schedule and does not produce the expected empty schedule list.

Related: #9488 describes similar schedule-removal behavior in Prefect 2.x and is closed. This report supplies a directly verified 3.8.5 CLI reproduction for an explicit empty list.