Public/Start-SqlServerAgentJob.ps1

function Start-SqlServerAgentJob {
    <#
    .SYNOPSIS
        Starts a SQL Server Agent job, synchronously or asynchronously.

    .EXAMPLE
        Start-SqlServerAgentJob -JobName 'MyJob' -ConnectionString $Tokens.DatabaseConfig.DatabaseDeploymentConnectionString
    #>

    [CmdletBinding()]
    [OutputType([void])]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', '')]
    param(
        # Name of the job to run.
        [Parameter(Mandatory=$true)]
        [string] 
        $JobName, 

        # Connection string that will be used to connect to the destination database.
        [Parameter(Mandatory=$true)]
        [string] 
        $ConnectionString,

        # The name of the step at which to begin execution of the job. If empty, job will start at first step.
        [Parameter(Mandatory=$false)]
        [string] 
        $StepName,

        # If $true, job will be run synchronously (will wait until it ends).
        [Parameter(Mandatory=$false)]
        [switch] 
        $Synchronous = $true,

        # Sleep interval when Synchronous is $true.
        [Parameter(Mandatory=$false)]
        [int] 
        $SleepIntervalInSeconds = 5,

        # If specified and Synchronous is $true, function will fail after TimeoutInSeconds seconds.
        [Parameter(Mandatory=$false)]
        [int] 
        $TimeoutInSeconds,

        # If $true and Synchronous if $true, function will check job outcome and fail if it's not 'succeeded' or timeout occurs.
        [Parameter(Mandatory=$false)]
        [switch] 
        $ValidateRunOutcome = $true,

        # Sql query timeout in seconds.
        [Parameter(Mandatory=$false)]
        [int] 
        $QueryTimeoutInSeconds
    )

    if ($Synchronous) {
        $syncLog = 'synchronously'
    } 
    else {
        $syncLog = 'asynchronously'
    }

    Write-Log -Info "Running SQL Server Agent job named '$JobName' $syncLog using connectionString '$ConnectionString'" -Emphasize

    $sqlParams = @{ 
        ConnectionString = $ConnectionString
        DatabaseName = ''
        SqlCommandMode = 'Scalar'
        Mode = '.net'
    }

    $jobId = Invoke-Sql @sqlParams -Query "select job_id from msdb.dbo.sysjobs where name = '$JobName'"
    if (!$jobId) {
        throw "Cannot find job named '$JobName' in msdb.dbo.sysjobs table."
    }

    $beforeRunMaxInstanceId = Invoke-Sql @sqlParams -Query "select max(isnull(instance_id, 0)) from msdb.dbo.sysjobhistory where job_id = '$jobId'"
    if (!$beforeRunMaxInstanceId -or $beforeRunMaxInstanceId -is [System.DBNull]) {
        $beforeRunMaxInstanceId = 0
    }

    $sql = "DECLARE @output int; EXEC @output = msdb.dbo.sp_start_job @job_name=N'$JobName'"
    if ($StepName) {
        $sql += ", @step_name=N'$StepName'"
    }
    $sql += "; SELECT @output"

    $result = Invoke-Sql @sqlParams -Query $sql
    if ($result -ne 0) {
        throw "Failed to start job '$JobName' - sp_start_job failed with result code $result"
    }

    if (!$Synchronous) {
        Write-Log -Info "Job '$JobName' has been started successfully."
        return
    }

    $runningSeconds = 0
    do {
        $maxInstanceId = Invoke-Sql @sqlParams -Query "select max(isnull(instance_id, 0)) from msdb.dbo.sysjobhistory where job_id = '$jobId'"
        if ($maxInstanceId -isnot [System.DBNull] -and $maxInstanceId -gt $beforeRunMaxInstanceId) {
            break
        }
        Write-Log -Info "Job '$JobName' is still running (waiting part 1)."
        Start-Sleep -Seconds $SleepIntervalInSeconds
        $runningSeconds += $SleepIntervalInSeconds
    } while (!$TimeoutInSeconds -or $runningSeconds -lt $TimeoutInSeconds)

    do {
        $sessionId = Invoke-Sql @sqlParams -Query `
        ("select top(1) session_id from msdb.dbo.sysjobactivity where job_id = '$jobId' and start_execution_date is not null and stop_execution_date is null " + `
        "order by start_execution_date desc")
        if (!$sessionId -or $sessionId -is [System.DBNull]) {
            break
        }
        Write-Log -Info "Job '$JobName' is still running (waiting part 2)."
        Start-Sleep -Seconds $SleepIntervalInSeconds
        $runningSeconds += $SleepIntervalInSeconds
    } while (!$TimeoutInSeconds -or $runningSeconds -lt $TimeoutInSeconds)

    if (!$ValidateRunOutcome) {
        Write-Log -Info "Job '$JobName' has finished. Run outcome has not been checked."
        return
    }

    if ($TimeoutInSeconds -and $runningSeconds -ge $TimeoutInSeconds) {
        Write-Log -Warn "Timeout occurred ($TimeoutInSeconds s)."
    }
    
    $sqlParams.SqlCommandMode = 'Dataset'

    $statusDataSet = (Invoke-Sql @sqlParams -Query "exec msdb.dbo.sp_help_job @job_name = '$JobName', @job_aspect = 'job'").Tables[0]
    $runDate = $statusDataSet.last_run_date
    $runTime = $statusDataSet.last_run_time
    $runOutcome = $statusDataSet.last_run_outcome
    $runOutcomeName = switch ($runOutcome) {
        0 { 'failed'; break; }
        1 { 'succeeded'; break; }
        3 { 'canceled'; break; }
        default { 'unknown' }
    }

    if ($runOutcome -ne 1) {
        Write-Log -Info "History $jobId / $runDate / $runTime"
        $historyInfo = Invoke-Sql @sqlParams -Query `
        ("select step_id, step_name, message from msdb.dbo.sysjobhistory where job_id = '$jobId' and " + `
         "msdb.dbo.agent_datetime(run_date,run_time) >= msdb.dbo.agent_datetime($runDate, $runTime) order by step_id")

        $log = "Job '$JobName' has failed (outcome $runOutcome = '$runOutcomeName'). Run history:`r`n"
        foreach ($historyEntry in $historyInfo.Tables[0]) {
            $log += "Step $($historyEntry.step_id). '$($historyEntry.step_name)': $($historyEntry.message)`r`n"
        }
        Write-Log -Warn $log
        throw "Job '$JobName' has failed - see messages above for details."
    }
    Write-Log -Info "Job '$JobName' has finished successfully."
}