Skip to content

Copy-DbaAgentSchedule - -Force never drops and recreates existing schedules #10594

Description

@greenmtnsun

Verified issue does not already exist?

I have searched and found no existing issue

What error did you receive?

No error. $error[0] is empty. -Force just silently does nothing.

The help for -Force (lines 43-45) says it "Forces the overwrite of existing schedules on the destination instances by dropping and recreating them". The drop never runs. And if the existing destination schedule has no jobs attached, nothing is emitted for it either, so the no-op doesn't show up in the output you would check afterwards.

Steps to Reproduce

# Schedule 'NightlyETL' exists on both instances. On sql02 it has no jobs attached.
# Expected per .PARAMETER Force: dropped and recreated on sql02.
# Actual: nothing emitted for it, destination schedule unchanged.
Copy-DbaAgentSchedule -Source sql01 -Destination sql02 -Force

To be straight about provenance: I found this by reading the source, not from a live migration, so the block above is the scenario the code path describes rather than a transcript of a run I made. The control-flow walk in "Other details" is verifiable from a clone at the refs I list there. Happy to run it against a live pair if that would help confirm.

Please confirm that you are running the most recent version of dbatools

Get-Module dbatools -ListAvailable | Select -ExpandProperty Version

Major  Minor  Build  Revision
-----  -----  -----  --------
2      1      5      -1

So no, my installed copy is not the latest. I checked the source at the published refs rather than relying on it.

The defect is present on the current release. public/Copy-DbaAgentSchedule.ps1 is byte-identical on v2.8.1, v2.8.2 and v2.8.4 (current, published 2026-07-31) and on development at 79e624a. The line numbers I cite below are from that file.

It is also present in the 2.1.5 I have installed. That file is not identical to current (it predates the $destServer.JobServer.SharedSchedules.Refresh() call and some surrounding restructuring), but the -Force arm is the same shape: the ShouldProcess wrapper at line 151, the unconditional continue at 158 and the unreachable drop at 160-163 correspond to lines 168, 175 and 177-180 in the current file.

Happy to upgrade and re-confirm on the latest if that would help.

Other details or mentions

The relevant part of public/Copy-DbaAgentSchedule.ps1:

123  if ($Force) { $ConfirmPreference = 'none' }              # begin block
158  if ($destSchedules.Name -contains $scheduleName) {
159      if ($Force -ne $true) { ...skip and report... continue }
167      } else {                                             # -Force
168          if ($Pscmdlet.ShouldProcess($destinstance, "Schedule [$scheduleName] has associated jobs. Skipping.")) {
169              if (...JobSchedules.Name -contains $scheduleName) {    # only reports if jobs exist
175              continue                                     # always, jobs or not
176          } else {                                         # only when ShouldProcess said no
177              if ($Pscmdlet.ShouldProcess($destinstance, "Dropping schedule $scheduleName and recreating")) {
180                  ...Drop()

In a normal run ShouldProcess returns true, so line 175 continues and the drop block at 177-186 is never reached. Line 123 makes that certain: -Force sets $ConfirmPreference = 'none', so the line 168 ShouldProcess cannot prompt and cannot return false. The only way I can find to reach Drop() is -Force -Confirm, answering No to "has associated jobs. Skipping." and then Yes to "Dropping schedule and recreating", which nobody is going to do by accident.

I think this is a regression rather than something that was never written. Before 9f67479 (2018-07-18, "hid object output in whatif") the -Force arm was:

else {                                                    # -Force
    if ($destServer.JobServer.Jobs.JobSchedules.Name -contains $scheduleName) {
        ...report Skipped...
        continue                                          # has jobs, skip
    }
    else {                                                # no jobs
        if ($Pscmdlet.ShouldProcess($destinstance, "Dropping schedule $scheduleName and recreating")) {
            $destServer.JobServer.SharedSchedules[$scheduleName].Drop()
        }
    }
}

That commit wrapped the associated-jobs test in a ShouldProcess call. The else that had belonged to the jobs test ended up attached to the new ShouldProcess instead, so "no associated jobs, drop it" became "user declined the prompt, drop it". Given the commit title was about WhatIf output, I doubt that was intended.

Two smaller things from the same structure. Schedules with no attached jobs produce no MigrationObject at all, because the reporting block at 170-173 is inside the jobs check while the continue at 175 is outside it. That continue moved to its current position in 2af6205 ("Standardize Whatif Output", #8846, 2023-04-09). And under -WhatIf both ShouldProcess messages print, so the output claims the schedule "has associated jobs" before anything has checked whether it does, and then claims it is "Dropping schedule ... and recreating" when it isn't.

For a fix, restoring the pre-9f67479581 shape would do it: check for associated jobs first with no ShouldProcess around the read, report Skipped and continue if there are any (the "cannot be overwritten, even with Force" case in help line 45), otherwise ShouldProcess("Dropping schedule $scheduleName and recreating"), drop, and fall through to the existing create block at line 192.

For transparency: this was surfaced by static analysis of the module, then verified by hand against the source and the git history before reporting.

What PowerShell host was used when producing this error

Windows PowerShell (powershell.exe), Windows PowerShell ISE (powershell_ise.exe)

PowerShell Host Version

Name                           Value
----                           -----
PSVersion                      5.1.26100.9168
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.26100.9168
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

SQL Server Edition and Build number

SQL Server 2022.

I have not pasted SELECT @@VERSION output because no live repro was run for this report (see Steps to Reproduce) — 2022 is simply the version I have available. The behaviour described is in the PowerShell control flow of the command and does not depend on the SQL Server version, edition or build. Happy to provide full @@VERSION output from a repro pair if you would like this confirmed live.

.NET Framework Version

.NET Framework 4.8.9337.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions