Aaron Bertrand

A script to script a script that scripts jobs

On this site, we usually talk about SQL Server performance problems that involve execution plans, locking, indexes, and waits. But SQL Server Agent can determine when backups, maintenance, ETL, monitoring, and reporting all arrive at the instance. In a way, a job deployment script is part of your performance configuration: you can materially impact the workload if you accidentally share a schedule across too many jobs.

I often create jobs in test environments that will later get distributed to many servers, often in isolated environments that can't see each other. Creating an idempotent script to create a job, but only if it doesn't already exist, can be cumbersome to get right. Many will prefer to use DbaTools Copy-DbaAgentJob, and that can be a great option if your deployment mechanism is PowerShell, DbaTools is already entrenched in your environment, and you can run the script from a place that can see the other server. In our case, T-SQL scripts are often passed on to colleagues or automation to be run independently in a different environment. Apologies to friends of SSMS, but the default right-click > Script Job output leaves a lot to be desired:

  • It hard-codes the schedule's schedule_uid, even though SQL Server can generate a new GUID when the schedule is created. This can complicate things because any changes you make will impact every other job that uses that schedule. I've seen performance tank because a weekly schedule for index maintenance was hijacked for another purpose and then changed to monthly.
  • It does not check whether the job already exists, so the script is not safely repeatable (we want idempotence here).
  • It uses GOTO labels for error handling instead of a more modern TRY/CATCH block.
  • It checks both @@ERROR and @ReturnCode after nearly every statement, creating a lot of noise around a relatively small amount of actual job configuration (most of which almost never fail).
  • It buries important job details in control flow, and the formatting is brutal, making changes difficult to review even in source control.
  • It uses local environment-specific values, like the owner, database, output file path, and others; these are valid on the source server but not necessarily on the destination.

I can clean this all up manually, of course, but doing that every time defeats much of the benefit of scripting the job in the first place. There isn't a way to make SSMS generate a more modern script that is less noisy or easier to understand and reuse. It takes effort to integrate the default output into other idempotent scripts so that, if someone accidentally hits execute twice, they're not left in an unknown state.

I wanted something I could call with a job name that would produce the script I actually wanted: idempotent, readable, free of GOTO, and without unnecessary GUIDs that force multiple jobs to synchronize to the same schedule. So, I wrote one.

I created this simple job to test it out, with two schedules and lots of single quotes to test my scripting skills:

Create a sample job 1Create a sample job 2Create a sample job 3Create a sample job 4Create a sample job 5Create a sample job 6

 
When scripting this job from Management Studio, here is the output (complete with gross M/D/Y output):
USE [msdb]
GO

/****** Object:  Job [You ain't got no job]    Script Date: 8/17/2026 6:42:53 PM ******/
BEGIN TRANSACTION
DECLARE @ReturnCode INT
SELECT @ReturnCode = 0
/****** Object:  JobCategory [Database Maintenance]    Script Date: 8/17/2026 6:42:53 PM ******/
IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'Database Maintenance' AND category_class=1)
BEGIN
EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'Database Maintenance'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback

END

DECLARE @jobId BINARY(16)
EXEC @ReturnCode =  msdb.dbo.sp_add_job @job_name=N'You ain''t got no job', 
		@enabled=1, 
		@notify_level_eventlog=0, 
		@notify_level_email=2, 
		@notify_level_netsend=0, 
		@notify_level_page=0, 
		@delete_level=0, 
		@description=N'This is just a job to test my job scripting stored procedure. It''s rad.', 
		@category_name=N'Database Maintenance', 
		@owner_login_name=N'sa', 
		@notify_email_operator_name=N'Failover Operators', @job_id = @jobId OUTPUT
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
/****** Object:  Step [Step 1 - Let's do this]    Script Date: 8/17/2026 6:42:53 PM ******/
EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Step 1 - Let''s do this', 
		@step_id=1, 
		@cmdexec_success_code=0, 
		@on_success_action=1, 
		@on_success_step_id=0, 
		@on_fail_action=2, 
		@on_fail_step_id=0, 
		@retry_attempts=2, 
		@retry_interval=1, 
		@os_run_priority=0, @subsystem=N'TSQL', 
		@command=N'SELECT ''Bobby O''''Connor is cool.'';', 
		@database_name=N'tempdb', 
		@output_file_name=N'C:\Temp\$(ESCAPE_NONE(JOBID)).log', 
		@flags=2
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'Schedule 1 - Let''s do this at midnight', 
		@enabled=1, 
		@freq_type=4, 
		@freq_interval=1, 
		@freq_subday_type=1, 
		@freq_subday_interval=0, 
		@freq_relative_interval=0, 
		@freq_recurrence_factor=0, 
		@active_start_date=20260731, 
		@active_end_date=99991231, 
		@active_start_time=0, 
		@active_end_time=235959, 
		@schedule_uid=N'558511df-e77d-41e2-a8c8-ee40676aa9d9'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id=@jobId, @name=N'Schedule 2 - Let''s do this at 4 AM', 
		@enabled=1, 
		@freq_type=4, 
		@freq_interval=1, 
		@freq_subday_type=1, 
		@freq_subday_interval=0, 
		@freq_relative_interval=0, 
		@freq_recurrence_factor=0, 
		@active_start_date=20260731, 
		@active_end_date=99991231, 
		@active_start_time=40000, 
		@active_end_time=235959, 
		@schedule_uid=N'0dd36ebd-4e8c-4985-925e-c47a9be61fb3'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
COMMIT TRANSACTION
GOTO EndSave
QuitWithRollback:
    IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION
EndSave:
GO

And here's the output from the procedure I wrote (listed below):

DECLARE @ReturnCode             int,
        @jobId                  binary(16),
        @error_message          nvarchar(2048),
        @owner_name             sysname,
        @email_operator_name    sysname = N'Failover Operators';

IF NOT EXISTS
(
    SELECT 1
      FROM msdb.dbo.sysjobs
     WHERE name = N'You ain''t got no job'
)
BEGIN
    IF @email_operator_name IS NOT NULL
       AND NOT EXISTS
           (
               SELECT 1
                 FROM msdb.dbo.sysoperators
                WHERE name = @email_operator_name
                  AND enabled = 1
           )
    BEGIN
        SET @error_message = N'email operator not found.';
        THROW 50101, @error_message, 1;
    END;

    -- Resolve the job owner at the destination.
    SELECT @owner_name = name
      FROM sys.server_principals
     WHERE [sid] = 0x01
       AND is_disabled = 0;

    IF @owner_name IS NULL
        THROW 50102, N'Destination job owner could not be resolved.', 1;

    IF @@TRANCOUNT > 0
        THROW 50103, N'This script cannot be executed inside an existing transaction.', 1;

    BEGIN TRY
        BEGIN TRANSACTION;

        SET @ReturnCode = 0;

        IF NOT EXISTS
        (
            SELECT 1
              FROM msdb.dbo.syscategories
             WHERE name = N'Database Maintenance'
               AND category_class = 1
        )
        BEGIN
            EXEC @ReturnCode = msdb.dbo.sp_add_category
                             @class = N'JOB',
                             @type  = N'LOCAL',
                             @name  = N'Database Maintenance';

            IF @ReturnCode <> 0
                THROW 50104, N'Failed to create job category.', 1;
        END;

        EXEC @ReturnCode = msdb.dbo.sp_add_job
             @job_name                     = N'You ain''t got no job',
             @enabled                      = 1,
             @notify_level_eventlog        = 0,
             @notify_level_email           = 2,
             @notify_email_operator_name   = @email_operator_name,
             @delete_level                 = 0,
             @description                  = N'This is just a job to test my job scripting stored procedure. It''s rad.',
             @category_name                = N'Database Maintenance',
             @owner_login_name             = @owner_name,
             @job_id                       = @jobId OUTPUT;

        IF @ReturnCode <> 0
            THROW 50105, N'Failed to create job.', 1;

        -- WARNING: This step writes output to a file.
        -- Verify that the path is valid on the destination
        -- and writable by the SQL Server Agent service account.
        EXEC @ReturnCode = msdb.dbo.sp_add_jobstep
             @job_id                = @jobId,
             @step_name             = N'Step 1 - Let''s do this',
             @step_id               = 1,
             @cmdexec_success_code  = 0,
             @on_success_action     = 1,
             @on_success_step_id    = 0,
             @on_fail_action        = 2,
             @on_fail_step_id       = 0,
             @retry_attempts        = 2,
             @retry_interval        = 1,
             @os_run_priority       = 0,
             @subsystem             = N'TSQL',
             @command               = N'SELECT ''Bobby O''''Connor is cool.'';',
             @additional_parameters = NULL,
             @server                = NULL,
             @database_name         = N'tempdb',
             @database_user_name    = NULL,
             @output_file_name      = N'C:\Temp\$(ESCAPE_NONE(JOBID)).log',
             @flags                 = 2;

        IF @ReturnCode <> 0
            THROW 50106, N'Failed to create step.', 1;

        EXEC @ReturnCode = msdb.dbo.sp_update_job
             @job_id       = @jobId,
             @start_step_id = 1;

        IF @ReturnCode <> 0
             THROW 50107, N'Failed to set start step.', 1;

        EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule
             @job_id                  = @jobId,
             @name                    = N'Schedule 2 - Let''s do this at 4 AM',
             @enabled                 = 1,
             @freq_type               = 4,
             @freq_interval           = 1,
             @freq_subday_type        = 1,
             @freq_subday_interval    = 0,
             @freq_relative_interval  = 0,
             @freq_recurrence_factor  = 0,
             @active_start_date       = 20260731,
             @active_end_date         = 99991231,
             @active_start_time       = 40000,
             @active_end_time         = 235959;

        IF @ReturnCode <> 0
           THROW 50108, N'Failed to create a job schedule.', 1;

        EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule
             @job_id                  = @jobId,
             @name                    = N'Schedule 1 - Let''s do this at midnight',
             @enabled                 = 1,
             @freq_type               = 4,
             @freq_interval           = 1,
             @freq_subday_type        = 1,
             @freq_subday_interval    = 0,
             @freq_relative_interval  = 0,
             @freq_recurrence_factor  = 0,
             @active_start_date       = 20260731,
             @active_end_date         = 99991231,
             @active_start_time       = 0,
             @active_end_time         = 235959;

        IF @ReturnCode <> 0
            THROW 50108, N'Failed to create a job schedule.', 1;

        EXEC @ReturnCode = msdb.dbo.sp_add_jobserver
            @job_id     = @jobId,
            @server_name = N'(local)';

        IF @ReturnCode <> 0
            THROW 50109, N'Failed to assign job to local server.', 1;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF XACT_STATE() <> 0
            ROLLBACK TRANSACTION;

        THROW;
    END CATCH;
END;

That is a longer script, for sure, than what SSMS provides. But IMHO it gives a lot more visibility and control over what you're executing, and is less of a "magic box" than a single-line PowerShell call. Some other benefits and deliberate behaviors:

  • Schedule and step GUIDs are omitted; SQL Server generates new ones every time.
  • For my scenario, I prefix the job name to the schedule name, to be sure there is no conflict (or confusion about which job the schedule belongs to).
  • Owners, databases, and other dependencies must already exist on the destination. I don't deal with proxies or net send/pager operators, but those could be added.
  • Multi-server jobs are rejected instead of being silently converted to local jobs.

The procedure is rather lengthy, so before I list it out, I'll tell you that I originally wrote one an even longer one that also scripted and validated proxies and the other operators (page and net send). I looked at the output and tried to remember if I had seen any of these in use since the 2000s, and I don't think I have. But if you are looking for that coverage, please let me know; I'm happy to share the longer version.

I'll also mention other future enhancements you might consider:

  • An option to keep the same schedule_uid (hey, you might want that).
  • An option to execute the output at {some linked server}.
  • An option to check the destination for too many jobs using a similar schedule. Even if you don't use the same schedule, if you set all of your jobs to run at midnight, don't be surprised at the storm that happens when they all kick off in unison.
  • You could even take that a bit further and match on more than just start time – look at history and see how many jobs run long enough to encroach into the new start time.
  • Outputting better comments (I currently output — single-line comments instead of /* block comments */).
  • More abstraction of variables so they can be set at the top of the output script, especially if repeated (e.g. category name).

The Procedure

CREATE OR ALTER PROCEDURE dbo.ScriptJob
    @job_name                 sysname,
    @script                   nvarchar(max) = NULL OUTPUT,
    @prefix_schedule_names    bit           = 0
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @job_id                     uniqueidentifier,
            /* I don't want '''' and '''''' and '''''''' everywhere if I can help it: */
            @crlf                       nchar(2) = NCHAR(13) + NCHAR(10),
            @q1                         nchar(1) = NCHAR(39),
            @q2                         nchar(2) = NCHAR(39) + NCHAR(39),
            @n                          nchar(1) = NCHAR(78),
            @null                       nchar(4) = N'NULL',
            @endcomma                   nchar(3) = NCHAR(44) + NCHAR(13) + NCHAR(10),
            @error_message              nvarchar(2048),
            @enabled                    tinyint,
            @description                nvarchar(512),
            @start_step_id              int,
            @category_name              sysname,
            @category_type              tinyint,
            @owner_login_name           sysname,
            @owner_sid                  varbinary(85),
            @notify_level_eventlog      int,
            @notify_level_email         int,
            @delete_level               int,
            @notify_email_operator_id   int,
            @email_operator_name        sysname,
            @has_local_target           bit = 0,
            /*
                @q_ means quoted and quote-escaped string literal
                A name could be 128 single quotes, so 128*2 + N'' = 259
            */
            @q_job_name                 nvarchar(259),
            @q_description              nvarchar(1027),
            @q_category_name            nvarchar(259),
            @q_owner_login_name         nvarchar(259),
            @q_email_operator_name      nvarchar(259),
            @q_category_type_name       nvarchar(15);

    /* Get job properties: */
    SELECT @job_id                     = j.job_id,
           @enabled                    = j.enabled,
           @description                = j.description,
           @start_step_id              = j.start_step_id,
           @category_name              = c.name,
           @category_type              = c.category_type,
           @owner_sid                  = j.owner_sid,
           @owner_login_name           = SUSER_SNAME(j.owner_sid),
           @notify_level_eventlog      = j.notify_level_eventlog,
           @notify_level_email         = j.notify_level_email,
           @delete_level               = j.delete_level,
           @notify_email_operator_id   = j.notify_email_operator_id,
           @email_operator_name        = email_operator.name
      FROM msdb.dbo.sysjobs AS j
     INNER JOIN msdb.dbo.syscategories AS c
        ON c.category_id = j.category_id
      LEFT OUTER JOIN msdb.dbo.sysoperators AS email_operator
        ON email_operator.id = j.notify_email_operator_id
     WHERE j.name = @job_name;

    IF @job_id IS NULL
    BEGIN
        SET @error_message = N'Job not found.';
        THROW 50001, @error_message, 1;
    END;

    /*
        sp_add_job requires an owner login name. If this can't be resolved,
        we can set it to sa, unless sa is disabled. This may not be desired
        behavior in your environment.
    */
    IF @owner_login_name IS NULL
    BEGIN
        IF @owner_sid <> 0x01
            SET @owner_sid = 0x01;

        SELECT @owner_login_name = name /* might not be sa! */
          FROM sys.server_principals
         WHERE [sid] = @owner_sid
           AND is_disabled = 0;

        IF @owner_login_name IS /* still */ NULL
        BEGIN
            SET @error_message = N'Owner SID not found.';
            THROW 50002, @error_message, 1;
        END;
    END;

    SET @prefix_schedule_names = COALESCE(@prefix_schedule_names, 0);

    /*
        Detect orphaned source operator references, separate from
        destination validation in the generated script. You may
        decide to just make a note that the operator isn't valid
        at the source and continue scripting.
    */
    IF (@notify_email_operator_id > 0 OR @notify_level_email > 0)
       AND @email_operator_name IS NULL
    BEGIN
        SET @error_message = N'email operator not found.';
        THROW 50003, @error_message, 1;
    END;

    /*
        This procedure scripts local jobs only.
        Multi-server jobs are more complicated.
    */
    IF EXISTS
    (
        SELECT 1 FROM msdb.dbo.sysjobservers
         WHERE job_id = @job_id
           AND server_id <> 0
    )
    BEGIN
        SET @error_message = N'This procedure only scripts local jobs.';
        THROW 50004, @error_message, 1;
    END;

    IF EXISTS
    (
        SELECT 1 FROM msdb.dbo.sysjobservers
         WHERE job_id = @job_id
           AND server_id = 0
    )
    BEGIN
        SET @has_local_target = 1;
    END;

    /*
        Schedule can't exceed 128 characters, so fail if longer.
        (Better than silently truncating and having collisions.)
    */
    IF @prefix_schedule_names = 1
    BEGIN
        IF EXISTS
        (
            SELECT 1 FROM msdb.dbo.sysjobschedules AS js
             INNER JOIN msdb.dbo.sysschedules AS s
                ON s.schedule_id = js.schedule_id
             WHERE js.job_id = @job_id
               AND DATALENGTH(CONCAT(@job_name, N' - ', s.name)) > 256
        )
        BEGIN
            SET @error_message = N'Prefixing the schedule name would silently truncate it.';
            THROW 50005, @error_message, 1;
        END;
    END;

    SET @q_category_type_name = CONCAT(@n, QUOTENAME(CASE @category_type
                              WHEN 2 THEN N'MULTI-SERVER'
                              WHEN 3 THEN N'NONE'
                              ELSE N'LOCAL' END, @q1));

    /*  Safely quote values, many of which are QUOTENAME-safe: */
    SET @q_job_name              = COALESCE(@n + QUOTENAME(@job_name,              @q1), @null);
    SET @q_category_name         = COALESCE(@n + QUOTENAME(@category_name,         @q1), @null);
    SET @q_owner_login_name      = COALESCE(@n + QUOTENAME(@owner_login_name,      @q1), @null);
    SET @q_email_operator_name   = COALESCE(@n + QUOTENAME(@email_operator_name,   @q1), @null);

    /*  But values that can exceed 128 characters need REPLACE(): */
    SET @q_description = COALESCE(@n + @q1 + REPLACE(@description, @q1, @q2) + @q1, @null);

    /*
        Begin the generated deployment script.

        Operator names are variables so that they can be changed for a
        destination environment without finding and editing sp_add_job.

        You may wish to abstract some other properties, too.
    */
    SET @script =
        CONCAT
        (
            CAST(N'' AS nvarchar(max)),

            N'DECLARE @ReturnCode             int,', @crlf,
            N'        @jobId                  binary(16),', @crlf,
            N'        @error_message          nvarchar(2048),', @crlf,
            N'        @owner_name             sysname,', @crlf,
            N'        @email_operator_name    sysname = ', @q_email_operator_name, N';', @crlf, @crlf,
            N'IF NOT EXISTS', @crlf,
            N'(', @crlf,
            N'    SELECT 1', @crlf,
            N'      FROM msdb.dbo.sysjobs', @crlf,
            N'     WHERE name = ', @q_job_name, @crlf,
            N')', @crlf,
            N'BEGIN', @crlf,
            N'    IF @email_operator_name IS NOT NULL', @crlf,
            N'       AND NOT EXISTS', @crlf,
            N'           (', @crlf,
            N'               SELECT 1', @crlf,
            N'                 FROM msdb.dbo.sysoperators', @crlf,
            N'                WHERE name = @email_operator_name', @crlf,
            N'                 AND enabled = 1', @crlf,
            N'           )', @crlf,
            N'    BEGIN', @crlf,
            N'        SET @error_message = N''email operator not found.'';', @crlf,
            N'        THROW 50101, @error_message, 1;', @crlf,
            N'    END;'
        );

    /*
        Resolve the owner at the destination. If the source owner is the
        principal with SID 0x01, resolve it by SID because it may have been
        renamed. Otherwise, try the source name and then fall back to the
        enabled principal with SID 0x01.
    */
    SET @script =
        CONCAT
        (
            @script, @crlf, @crlf,
            N'    -- Resolve the job owner at the destination.', @crlf,
            N'    SELECT @owner_name = name', @crlf,
            N'      FROM sys.server_principals', @crlf,
            CASE
                WHEN @owner_sid = 0x01
                    THEN N'     WHERE [sid] = 0x01'
                ELSE CONCAT(N'     WHERE name = ', @q_owner_login_name)
            END,
            @crlf,
            N'       AND is_disabled = 0;', @crlf,
            CASE
                WHEN @owner_sid <> 0x01
                    THEN CONCAT
                         (
                             @crlf,
                             N'    IF @owner_name IS NULL', @crlf,
                             N'    BEGIN', @crlf,
                             N'        SELECT @owner_name = name', @crlf,
                             N'          FROM sys.server_principals', @crlf,
                             N'         WHERE [sid] = 0x01', @crlf,
                             N'           AND is_disabled = 0;', @crlf,
                             N'    END;', @crlf
                         )
                ELSE N''
            END,
            @crlf,
            N'    IF @owner_name IS NULL', @crlf,
            N'        THROW 50102, N''Destination job owner could not be resolved.'', 1;'
        );

    /*
        Generate the transaction, category, and job creation.
        Abort if the generated script is executed inside an existing transaction.
    */
    SET @script =
        CONCAT
        (
            @script, @crlf, @crlf,
            N'    IF @@TRANCOUNT > 0', @crlf,
            N'        THROW 50103, N''This script cannot be executed inside an existing transaction.'', 1;', @crlf, @crlf,
            N'    BEGIN TRY', @crlf,
            N'        BEGIN TRANSACTION;', @crlf, @crlf,
            N'        SET @ReturnCode = 0;', @crlf, @crlf,
            N'        IF NOT EXISTS', @crlf,
            N'        (', @crlf,
            N'            SELECT 1', @crlf,
            N'              FROM msdb.dbo.syscategories', @crlf,
            N'             WHERE name = ', @q_category_name, @crlf,
            N'               AND category_class = 1', @crlf,
            N'        )', @crlf,
            N'        BEGIN', @crlf,
            N'            EXEC @ReturnCode = msdb.dbo.sp_add_category', @crlf,
            N'                             @class = N''JOB'',', @crlf,
            N'                             @type  = ', @q_category_type_name, @endcomma,
            N'                             @name  = ', @q_category_name, N';', @crlf, @crlf,
            N'            IF @ReturnCode <> 0', @crlf,
            N'                THROW 50104, ', N'N''Failed to create job category.'', 1;', @crlf,
            N'        END;', @crlf, @crlf,
            N'        EXEC @ReturnCode = msdb.dbo.sp_add_job', @crlf,
            N'             @job_name                     = ', @q_job_name, @endcomma,
            N'             @enabled                      = ', @enabled, @endcomma,
            N'             @notify_level_eventlog        = ', @notify_level_eventlog, @endcomma,
            N'             @notify_level_email           = ', @notify_level_email, @endcomma,
            N'             @notify_email_operator_name   = @email_operator_name,', @crlf,
            N'             @delete_level                 = ', @delete_level, @endcomma,
            N'             @description                  = ', @q_description, @endcomma,
            N'             @category_name                = ', @q_category_name, @endcomma,
            N'             @owner_login_name             = @owner_name,', @crlf,
            N'             @job_id                       = @jobId OUTPUT;', @crlf, @crlf,
            N'        IF @ReturnCode <> 0', @crlf,
            N'            THROW 50105, ', N'N''Failed to create job.'', 1;'
        );

    /*
        Script the job steps.
    */
    DECLARE @step_id               int,
            @step_name             sysname,
            @subsystem             nvarchar(40),
            @command               nvarchar(max),
            @flags                 int,
            @additional_parameters nvarchar(max),
            @cmdexec_success_code  int,
            @on_success_action     tinyint,
            @on_success_step_id    int,
            @on_fail_action        tinyint,
            @on_fail_step_id       int,
            @step_server           sysname,
            @database_name         sysname,
            @database_user_name    sysname,
            @retry_attempts        int,
            @retry_interval        int,
            @os_run_priority       int,
            @output_file_name      nvarchar(200),
            @q_step_name           nvarchar(259),
            @q_subsystem           nvarchar(83),
            @q_command             nvarchar(max),
            @q_parameters          nvarchar(max),
            @q_step_server         nvarchar(259),
            @q_database_name       nvarchar(259),
            @q_db_user_name        nvarchar(259),
            @q_output_file         nvarchar(403);

    DECLARE @jobsteps cursor;

    SET @jobsteps = CURSOR LOCAL FAST_FORWARD FOR
           SELECT js.step_id,
                  js.step_name,
                  js.subsystem,
                  js.command,
                  js.flags,
                  CONVERT(nvarchar(max), js.additional_parameters),
                  js.cmdexec_success_code,
                  js.on_success_action,
                  js.on_success_step_id,
                  js.on_fail_action,
                  js.on_fail_step_id,
                  js.server,
                  js.database_name,
                  js.database_user_name,
                  js.retry_attempts,
                  js.retry_interval,
                  js.os_run_priority,
                  js.output_file_name
             FROM msdb.dbo.sysjobsteps AS js
            WHERE js.job_id = @job_id
            ORDER BY js.step_id;

    OPEN @jobsteps;

    FETCH NEXT FROM @jobsteps
     INTO @step_id,
          @step_name,
          @subsystem,
          @command,
          @flags,
          @additional_parameters,
          @cmdexec_success_code,
          @on_success_action,
          @on_success_step_id,
          @on_fail_action,
          @on_fail_step_id,
          @step_server,
          @database_name,
          @database_user_name,
          @retry_attempts,
          @retry_interval,
          @os_run_priority,
          @output_file_name;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @q_step_name     = CONCAT(@n, QUOTENAME(@step_name, @q1));
        SET @q_subsystem     = CONCAT(@n, QUOTENAME(@subsystem, @q1));
        SET @q_step_server   = COALESCE(@n + QUOTENAME(@step_server,   @q1), @null);
        SET @q_database_name = COALESCE(@n + QUOTENAME(@database_name, @q1), @null);
        SET @q_db_user_name  = COALESCE(@n + QUOTENAME(@database_user_name, @q1), @null);
        SET @q_command       = COALESCE(@n + @q1 + REPLACE(@command, @q1, @q2) + @q1, @null);
        SET @q_parameters    = COALESCE(@n + @q1 + REPLACE(@additional_parameters, @q1, @q2) + @q1, @null);
        SET @q_output_file   = COALESCE(@n + @q1 + REPLACE(@output_file_name, @q1, @q2) + @q1, @null);

        SET @script =
            CONCAT
            (
                @script, @crlf, @crlf,
                CASE WHEN @output_file_name > N''
                     THEN CONCAT
                          (
                              N'        -- WARNING: This step writes output to a file.', @crlf,
                              N'        -- Verify that the path is valid on the destination', @crlf,
                              N'        -- and writable by the SQL Server Agent service account.', @crlf
                          )
                     ELSE N''
                     END,
                CASE WHEN DATALENGTH(@output_file_name) = 400
                     THEN CONCAT
                          (
                              N'        -- WARNING: output_file_name uses the full nvarchar(200).', @crlf,
                              N'        -- The value may have been truncated before ',
                              N'being stored; verify the complete path.', @crlf
                          )
                     ELSE N''
                     END,
                CASE WHEN @database_name IS NOT NULL
                      AND @database_name NOT IN (N'master', N'msdb', N'tempdb', N'model')
                     THEN CONCAT
                          (
                              N'        -- WARNING: database_name may not be present at the destination.', @crlf
                          )
                     ELSE N''
                     END,
                CASE WHEN @database_user_name IS NOT NULL
                     THEN CONCAT
                          (
                              N'        -- WARNING: database_user_name may not be present at the destination.', @crlf
                          )
                     ELSE N''
                     END,
                N'        EXEC @ReturnCode = msdb.dbo.sp_add_jobstep', @crlf,
                N'             @job_id                = @jobId,', @crlf,
                N'             @step_name             = ', @q_step_name,          @endcomma,
                N'             @step_id               = ', @step_id,              @endcomma,
                N'             @cmdexec_success_code  = ', @cmdexec_success_code, @endcomma,
                N'             @on_success_action     = ', @on_success_action,    @endcomma,
                N'             @on_success_step_id    = ', @on_success_step_id,   @endcomma,
                N'             @on_fail_action        = ', @on_fail_action,       @endcomma,
                N'             @on_fail_step_id       = ', @on_fail_step_id,      @endcomma,
                N'             @retry_attempts        = ', @retry_attempts,       @endcomma,
                N'             @retry_interval        = ', @retry_interval,       @endcomma,
                N'             @os_run_priority       = ', @os_run_priority,      @endcomma,
                N'             @subsystem             = ', @q_subsystem,          @endcomma,
                N'             @command               = ', @q_command,            @endcomma,
                N'             @additional_parameters = ', @q_parameters,         @endcomma,
                N'             @server                = ', @q_step_server,        @endcomma,
                N'             @database_name         = ', @q_database_name,      @endcomma,
                N'             @database_user_name    = ', @q_db_user_name,       @endcomma,
                N'             @output_file_name      = ', @q_output_file,        @endcomma,
                N'             @flags                 = ', @flags, N';', @crlf, @crlf,
                N'        IF @ReturnCode <> 0', @crlf,
                N'            THROW 50106, N''Failed to create step.'', 1;'
            );

        FETCH NEXT FROM @jobsteps
         INTO @step_id,
              @step_name,
              @subsystem,
              @command,
              @flags,
              @additional_parameters,
              @cmdexec_success_code,
              @on_success_action,
              @on_success_step_id,
              @on_fail_action,
              @on_fail_step_id,
              @step_server,
              @database_name,
              @database_user_name,
              @retry_attempts,
              @retry_interval,
              @os_run_priority,
              @output_file_name;
    END;

    /*
        Set the starting step after all steps have been created.
    */
    SET @script =
        CONCAT
        (
            @script, @crlf, @crlf,
            N'        EXEC @ReturnCode = msdb.dbo.sp_update_job', @crlf,
            N'            @job_id       = @jobId,', @crlf,
            N'            @start_step_id = ', @start_step_id, N';', @crlf, @crlf,
            N'        IF @ReturnCode <> 0', @crlf,
            N'            THROW 50107, N''Failed to set start step.'', 1;'
        );

    /*
        Script all schedules attached to the job. If requested, prefix each
        generated schedule name with the job name for easier identification.

        schedule_uid is intentionally omitted. SQL Server generates a new
        schedule that is initially attached only to this job. If the source
        schedule is shared, the generated script creates an independent copy.
    */
    DECLARE @schedule_name           sysname,
            @scripted_schedule_name  sysname,
            @schedule_enabled        int,
            @freq_type               int,
            @freq_interval           int,
            @freq_subday_type        int,
            @freq_subday_interval    int,
            @freq_relative_interval  int,
            @freq_recurrence_factor  int,
            @active_start_date       int,
            @active_end_date         int,
            @active_start_time       int,
            @active_end_time         int,
            @jobs_using_schedule     int,
            @q_schedule_name         nvarchar(259);

    DECLARE @schedules cursor;

    SET @schedules = CURSOR LOCAL FAST_FORWARD FOR
            SELECT s.name,
                   s.enabled,
                   s.freq_type,
                   s.freq_interval,
                   s.freq_subday_type,
                   s.freq_subday_interval,
                   s.freq_relative_interval,
                   s.freq_recurrence_factor,
                   s.active_start_date,
                   s.active_end_date,
                   s.active_start_time,
                   s.active_end_time,
                   jobs_using_schedule =
                   (
                       SELECT COUNT(*)
                         FROM msdb.dbo.sysjobschedules AS sch
                        WHERE sch.schedule_id = s.schedule_id
                   )
              FROM msdb.dbo.sysjobschedules AS js
             INNER JOIN msdb.dbo.sysschedules AS s
                ON s.schedule_id = js.schedule_id
             WHERE js.job_id = @job_id
             ORDER BY s.schedule_id;

    OPEN @schedules;

    FETCH NEXT FROM @schedules
     INTO @schedule_name,
          @schedule_enabled,
          @freq_type,
          @freq_interval,
          @freq_subday_type,
          @freq_subday_interval,
          @freq_relative_interval,
          @freq_recurrence_factor,
          @active_start_date,
          @active_end_date,
          @active_start_time,
          @active_end_time,
          @jobs_using_schedule;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @scripted_schedule_name =
            CASE @prefix_schedule_names
                WHEN 1 THEN CONCAT(@job_name, N' - ') ELSE N'' END
                + @schedule_name;

        SET @q_schedule_name = CONCAT(@n, QUOTENAME(@scripted_schedule_name, @q1));

        SET @script =
            CONCAT
            (
                @script, @crlf, @crlf,
                CASE WHEN @jobs_using_schedule > 1
                     THEN CONCAT
                          (N'        -- The source schedule was shared by multiple jobs;', @crlf,
                           N'        -- this creates an independent copy.', @crlf)
                     ELSE N''
                     END,
                N'        EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule', @crlf,
                N'            @job_id                  = @jobId,', @crlf,
                N'            @name                    = ', @q_schedule_name, @endcomma,
                N'            @enabled                 = ', @schedule_enabled, @endcomma,
                N'            @freq_type               = ', @freq_type, @endcomma,
                N'            @freq_interval           = ', @freq_interval, @endcomma,
                N'            @freq_subday_type        = ', @freq_subday_type, @endcomma,
                N'            @freq_subday_interval    = ', @freq_subday_interval, @endcomma,
                N'            @freq_relative_interval  = ', @freq_relative_interval, @endcomma,
                N'            @freq_recurrence_factor  = ', @freq_recurrence_factor, @endcomma,
                N'            @active_start_date       = ', @active_start_date, @endcomma,
                N'            @active_end_date         = ', @active_end_date, @endcomma,
                N'            @active_start_time       = ', @active_start_time, @endcomma,
                N'            @active_end_time         = ', @active_end_time, N';', @crlf, @crlf,
                N'        IF @ReturnCode <> 0', @crlf,
                N'            THROW 50108, N''Failed to create a job schedule.'', 1;'
            );

        FETCH NEXT FROM @schedules
         INTO @schedule_name,
              @schedule_enabled,
              @freq_type,
              @freq_interval,
              @freq_subday_type,
              @freq_subday_interval,
              @freq_relative_interval,
              @freq_recurrence_factor,
              @active_start_date,
              @active_end_date,
              @active_start_time,
              @active_end_time,
              @jobs_using_schedule;
    END;

    /*
        Preserve the local server assignment.
    */
    IF @has_local_target = 1
    BEGIN
        SET @script =
            CONCAT
            (
                @script, @crlf, @crlf,
                N'        EXEC @ReturnCode = msdb.dbo.sp_add_jobserver', @crlf,
                N'            @job_id     = @jobId,', @crlf,
                N'            @server_name = N''(local)'';', @crlf, @crlf,
                N'        IF @ReturnCode <> 0', @crlf,
                N'            THROW 50109, N''Failed to assign job to local server.'', 1;'
            );
    END;

    /*
        Clean up.
    */
    SET @script =
        CONCAT
        (
            @script, @crlf, @crlf,
            N'        COMMIT TRANSACTION;', @crlf,
            N'    END TRY', @crlf,
            N'    BEGIN CATCH', @crlf,
            N'        IF XACT_STATE() <> 0', @crlf,
            N'            ROLLBACK TRANSACTION;', @crlf, @crlf,
            N'        THROW;', @crlf,
            N'    END CATCH;', @crlf,
            N'END;', @crlf
        );

    SELECT Job   = @script,
    /* In case SSMS truncates output, thank Erik Darling https://dba.stackexchange.com/a/205733 */
           AsXML = (SELECT @crlf + @crlf + @script + @crlf AS [processing-instruction(_)] FOR XML PATH(''));
END;