Conclusion
Use a single script to batch export and recreate SQL Agent Jobs, including steps, schedules, and notifications, saving time and preventing missed configurations.
Where It’s Useful
- DB migration / environment switching (DEV → UAT → PROD)
- Multiple SQL Agent Jobs need to be recreated simultaneously
- To avoid missing steps or schedules when manually creating Jobs
- For DBAs / backend engineers to automate operations
Process Steps
1. Define Parameters and Job List
First, centralize the management of changeable settings (DB, Owner, whether to overwrite), then use a table variable to define the list of Jobs to be exported, supporting renaming. This allows processing multiple Jobs at once without repetitive operations.
DECLARE @OverrideDatabaseName SYSNAME = N'你的新DB'; -- 指定新 DBDECLARE @OverrideOwnerLoginName SYSNAME = N'sa'; -- 指定 owner
INSERT INTO @Jobs (SourceJobName, TargetJobName)VALUES (N'Job_A', N'Job_A_New'), (N'Job_B', NULL);2. Perform Checks to Prevent Export Errors
Before generating the script, verify if the source Job exists and if the target name is duplicated. This prevents discovering errors halfway through the migration.
IF EXISTS ( SELECT 1 FROM @Jobs j LEFT JOIN msdb.dbo.sysjobs sj ON sj.name = j.SourceJobName WHERE sj.job_id IS NULL)BEGIN RAISERROR(N'Job 不存在', 16, 1); RETURN;END;3. Dynamically Assemble Job / Step / Schedule
Using system tables msdb.dbo.sysjobs, sysjobsteps, and sysschedules, assemble the complete script step by step. This ensures that the recreated Job behaves identically to the original.
EXEC msdb.dbo.sp_add_job @job_name = N'Job_A_New', @enabled = 1;
EXEC msdb.dbo.sp_add_jobstep @job_id = @jobId, @step_name = N'Step1', @command = N'SELECT 1;', @database_name = N'你的新DB';4. Output Warnings and Check Information
The script will additionally flag risks, such as non-TSQL steps, Proxies, hardcoded DB names, etc., allowing you to check them before deployment.
-- WARNING: 非 TSQL step-- WARNING: 使用 Proxy-- WARNING: command 可能寫死 DBAdditional Notes
- If a DB name is hardcoded within the command, the script will not automatically change it; manual adjustment is required
- For Proxy / SSIS / PowerShell steps, corresponding configurations must first be set up in the new environment
- In a production environment, it is recommended not to directly overwrite Jobs; confirm before execution
Commands / Examples
Click to expand
USE msdb;GO
SET NOCOUNT ON;
--------------------------------------------------------------------------------- 參數區-------------------------------------------------------------------------------DECLARE @OverrideDatabaseName SYSNAME = N'你的新DB'; -- NULL = 不覆蓋 step database_nameDECLARE @OverrideOwnerLoginName SYSNAME = N'sa'; -- NULL = 沿用原 ownerDECLARE @OverrideOperatorName SYSNAME = NULL; -- NULL = 沿用原 operatorDECLARE @DropIfExists BIT = 1; -- 1=若同名 Job 已存在,先刪除再重建DECLARE @AttachToLocalServer BIT = 1; -- 1=最後執行 sp_add_jobserver 綁到 (LOCAL)
DECLARE @CRLF NVARCHAR(10) = CHAR(13) + CHAR(10) + '/**/';DECLARE @FinalSQL NVARCHAR(MAX) = N'';DECLARE @Header NVARCHAR(MAX) = N'';
--------------------------------------------------------------------------------- 指定要匯出的 Jobs-------------------------------------------------------------------------------DECLARE @Jobs TABLE( RowNum INT IDENTITY(1,1) PRIMARY KEY, SourceJobName SYSNAME NOT NULL, TargetJobName SYSNAME NULL);
-- ===== 在這裡填你要匯出的 Jobs =====INSERT INTO @Jobs (SourceJobName, TargetJobName)VALUES (N'Job_A', N'Job_A_New'), (N'Job_B', NULL), (N'Job_C', N'Job_C_New');-- ==================================
--------------------------------------------------------------------------------- 基本檢查:來源 Job 是否存在-------------------------------------------------------------------------------IF EXISTS( SELECT 1 FROM @Jobs j LEFT JOIN msdb.dbo.sysjobs sj ON sj.name = j.SourceJobName WHERE sj.job_id IS NULL)BEGIN SELECT j.SourceJobName FROM @Jobs j LEFT JOIN msdb.dbo.sysjobs sj ON sj.name = j.SourceJobName WHERE sj.job_id IS NULL;
RAISERROR(N'上面列出的 SourceJobName 在 msdb.dbo.sysjobs 中找不到,請先修正。', 16, 1); RETURN;END;
--------------------------------------------------------------------------------- 檢查 TargetJobName 是否重複-------------------------------------------------------------------------------IF EXISTS( SELECT 1 FROM ( SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName FROM @Jobs ) x GROUP BY x.FinalJobName HAVING COUNT(*) > 1)BEGIN SELECT x.FinalJobName, COUNT(*) AS Cnt FROM ( SELECT ISNULL(TargetJobName, SourceJobName) AS FinalJobName FROM @Jobs ) x GROUP BY x.FinalJobName HAVING COUNT(*) > 1;
RAISERROR(N'上面列出的 TargetJobName / FinalJobName 有重複,會互相覆蓋,請先修正。', 16, 1); RETURN;END;
--------------------------------------------------------------------------------- Header-------------------------------------------------------------------------------SET @Header =N'/* =====================================================================' + @CRLF +N' SQL Agent Multi-Job Export Script' + @CRLF +N' Generated : ' + CONVERT(NVARCHAR(19), GETDATE(), 120) + @CRLF +N' Notes:' + @CRLF +N' 1. 請先確認新環境是否存在對應 Login / Operator / Proxy / Database。' + @CRLF +N' 2. 若 step command 內文有寫死舊 DB,需人工調整。' + @CRLF +N' 3. 建議先建立後停用 Job,手動測試 step 無誤再啟用 schedule。' + @CRLF +N'===================================================================== */' + @CRLF + @CRLF;
SET @FinalSQL = @Header;
--------------------------------------------------------------------------------- 先準備待處理 Job 清單-------------------------------------------------------------------------------DECLARE @JobList TABLE( RowNum INT PRIMARY KEY, SourceJobName SYSNAME NOT NULL, TargetJobName SYSNAME NOT NULL, JobId UNIQUEIDENTIFIER NOT NULL);
INSERT INTO @JobList (RowNum, SourceJobName, TargetJobName, JobId)SELECT j.RowNum, j.SourceJobName, ISNULL(j.TargetJobName, j.SourceJobName) AS TargetJobName, sj.job_idFROM @Jobs jJOIN msdb.dbo.sysjobs sj ON sj.name = j.SourceJobName;
--------------------------------------------------------------------------------- 逐一組 Script-------------------------------------------------------------------------------DECLARE @RowNum INT, @SourceJobName SYSNAME, @TargetJobName SYSNAME, @JobId UNIQUEIDENTIFIER, @JobSQL NVARCHAR(MAX), @Warnings NVARCHAR(MAX), @Tmp NVARCHAR(MAX);
DECLARE JobCursor CURSOR FAST_FORWARD FORSELECT RowNum, SourceJobName, TargetJobName, JobIdFROM @JobListORDER BY RowNum;
OPEN JobCursor;FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;
WHILE @@FETCH_STATUS = 0BEGIN SET @JobSQL = N''; SET @Warnings = N''; SET @Tmp = N'';
--------------------------------------------------------------------------- -- Job 區塊標頭 --------------------------------------------------------------------------- SET @JobSQL = @JobSQL + N'/* =====================================================================' + @CRLF + N' Job #' + CAST(@RowNum AS NVARCHAR(10)) + N'' + @CRLF + N' Source Job : ' + REPLACE(@SourceJobName, '*/', '* /') + @CRLF + N' Target Job : ' + REPLACE(@TargetJobName, '*/', '* /') + @CRLF + N'===================================================================== */' + @CRLF + @CRLF;
--------------------------------------------------------------------------- -- Warning 區 --------------------------------------------------------------------------- IF EXISTS ( SELECT 1 FROM msdb.dbo.sysjobsteps WHERE job_id = @JobId GROUP BY job_id HAVING COUNT(*) > 1 ) BEGIN SET @Warnings = @Warnings + N'-- WARNING: 此 Job 含多個 steps,請逐一確認 step 間跳轉。' + @CRLF; END;
IF EXISTS ( SELECT 1 FROM msdb.dbo.sysjobsteps WHERE job_id = @JobId AND subsystem <> N'TSQL' ) BEGIN SET @Warnings = @Warnings + N'-- WARNING: 含非 TSQL steps(如 CmdExec / PowerShell / SSIS),新環境需確認元件與權限。' + @CRLF; END;
IF EXISTS ( SELECT 1 FROM msdb.dbo.sysjobsteps WHERE job_id = @JobId AND proxy_id IS NOT NULL AND proxy_id <> 0 ) BEGIN SET @Warnings = @Warnings + N'-- WARNING: 此 Job 使用 Proxy,新環境必須先建立對應 Proxy/Credential。' + @CRLF; END;
IF EXISTS ( SELECT 1 FROM msdb.dbo.sysjobsteps WHERE job_id = @JobId AND ( command LIKE N'%USE [%]%' OR command LIKE N'%].dbo.%' OR command LIKE N'%.dbo.%' ) ) BEGIN SET @Warnings = @Warnings + N'-- WARNING: 某些 step command 可能寫死資料庫名稱,請人工檢查 command 內容。' + @CRLF; END;
IF @Warnings <> N'' BEGIN SET @JobSQL = @JobSQL + N'/* Pre-check warnings */' + @CRLF + @Warnings + @CRLF; END;
--------------------------------------------------------------------------- -- 刪除同名 Job(可選) --------------------------------------------------------------------------- SET @JobSQL = @JobSQL + N'USE msdb;' + @CRLF + N'GO' + @CRLF;
IF @DropIfExists = 1 BEGIN SET @JobSQL = @JobSQL + N'IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''')' + @CRLF + N'BEGIN' + @CRLF + N' EXEC msdb.dbo.sp_delete_job @job_name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''';' + @CRLF + N'END' + @CRLF + N'GO' + @CRLF; END;
SET @JobSQL = @JobSQL + N'DECLARE @jobId BINARY(16);' + @CRLF + @CRLF;
--------------------------------------------------------------------------- -- sp_add_job --------------------------------------------------------------------------- SELECT @Tmp = N'EXEC msdb.dbo.sp_add_job' + @CRLF + N' @job_name = N''' + REPLACE(@TargetJobName, '''', '''''') + N''',' + @CRLF + N' @enabled = ' + CAST(j.enabled AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_eventlog = ' + CAST(j.notify_level_eventlog AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_email = ' + CAST(j.notify_level_email AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_netsend = ' + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_page = ' + CAST(j.notify_level_page AS NVARCHAR(10)) + N',' + @CRLF + N' @delete_level = ' + CAST(j.delete_level AS NVARCHAR(10)) + N',' + @CRLF + N' @description = N''' + REPLACE(ISNULL(j.description, N''), '''', '''''') + N''',' + @CRLF + N' @category_name = N''' + REPLACE(ISNULL(c.name, N'[Uncategorized (Local)]'), '''', '''''') + N''',' + @CRLF + N' @owner_login_name = N''' + REPLACE(ISNULL(@OverrideOwnerLoginName, SUSER_SNAME(j.owner_sid)), '''', '''''') + N''',' + @CRLF + N' @job_id = @jobId OUTPUT;' + @CRLF + @CRLF FROM msdb.dbo.sysjobs j LEFT JOIN msdb.dbo.syscategories c ON j.category_id = c.category_id WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
--------------------------------------------------------------------------- -- Steps --------------------------------------------------------------------------- SET @Tmp = N'';
SELECT @Tmp = @Tmp + N'EXEC msdb.dbo.sp_add_jobstep' + @CRLF + N' @job_id = @jobId,' + @CRLF + N' @step_id = ' + CAST(s.step_id AS NVARCHAR(10)) + N',' + @CRLF + N' @step_name = N''' + REPLACE(s.step_name, '''', '''''') + N''',' + @CRLF + N' @subsystem = N''' + REPLACE(s.subsystem, '''', '''''') + N''',' + @CRLF + N' @command = N''' + REPLACE(ISNULL(s.command, N''), '''', '''''') + N''',' + @CRLF + CASE WHEN s.additional_parameters IS NULL THEN N'' ELSE N' @additional_parameters = N''' + REPLACE(s.additional_parameters, '''', '''''') + N''',' + @CRLF END + N' @cmdexec_success_code = ' + CAST(ISNULL(s.cmdexec_success_code, 0) AS NVARCHAR(10)) + N',' + @CRLF + N' @on_success_action = ' + CAST(s.on_success_action AS NVARCHAR(10)) + N',' + @CRLF + N' @on_success_step_id = ' + CAST(s.on_success_step_id AS NVARCHAR(10)) + N',' + @CRLF + N' @on_fail_action = ' + CAST(s.on_fail_action AS NVARCHAR(10)) + N',' + @CRLF + N' @on_fail_step_id = ' + CAST(s.on_fail_step_id AS NVARCHAR(10)) + N',' + @CRLF + CASE WHEN s.server IS NULL THEN N'' ELSE N' @server = N''' + REPLACE(s.server, '''', '''''') + N''',' + @CRLF END + N' @database_name = ' + CASE WHEN ISNULL(@OverrideDatabaseName, s.database_name) IS NULL THEN N'NULL' ELSE N'N''' + REPLACE(ISNULL(@OverrideDatabaseName, s.database_name), '''', '''''') + N'''' END + N',' + @CRLF + N' @database_user_name = ' + CASE WHEN s.database_user_name IS NULL THEN N'NULL' ELSE N'N''' + REPLACE(s.database_user_name, '''', '''''') + N'''' END + N',' + @CRLF + N' @retry_attempts = ' + CAST(ISNULL(s.retry_attempts, 0) AS NVARCHAR(10)) + N',' + @CRLF + N' @retry_interval = ' + CAST(ISNULL(s.retry_interval, 0) AS NVARCHAR(10)) + N',' + @CRLF + N' @os_run_priority = ' + CAST(ISNULL(s.os_run_priority, 0) AS NVARCHAR(10)) + N',' + @CRLF + N' @output_file_name = ' + CASE WHEN s.output_file_name IS NULL THEN N'NULL' ELSE N'N''' + REPLACE(s.output_file_name, '''', '''''') + N'''' END + N',' + @CRLF + CASE WHEN p.name IS NULL OR p.name = N'' THEN N'' ELSE N' @proxy_name = N''' + REPLACE(p.name, '''', '''''') + N''',' + @CRLF END + N' @flags = ' + CAST(ISNULL(s.flags, 0) AS NVARCHAR(10)) + N';' + @CRLF + @CRLF FROM msdb.dbo.sysjobsteps s LEFT JOIN msdb.dbo.sysproxies p ON s.proxy_id = p.proxy_id WHERE s.job_id = @JobId ORDER BY s.step_id;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
--------------------------------------------------------------------------- -- start_step_id --------------------------------------------------------------------------- SELECT @Tmp = N'EXEC msdb.dbo.sp_update_job' + @CRLF + N' @job_id = @jobId,' + @CRLF + N' @start_step_id = ' + CAST(j.start_step_id AS NVARCHAR(10)) + N';' + @CRLF + @CRLF FROM msdb.dbo.sysjobs j WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
--------------------------------------------------------------------------- -- schedules --------------------------------------------------------------------------- SET @Tmp = N'';
SELECT @Tmp = @Tmp + N'EXEC msdb.dbo.sp_add_jobschedule' + @CRLF + N' @job_id = @jobId,' + @CRLF + N' @name = N''' + REPLACE(sch.name, '''', '''''') + N''',' + @CRLF + N' @enabled = ' + CAST(sch.enabled AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_type = ' + CAST(sch.freq_type AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_interval = ' + CAST(sch.freq_interval AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_subday_type = ' + CAST(sch.freq_subday_type AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_subday_interval = ' + CAST(sch.freq_subday_interval AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_relative_interval = ' + CAST(sch.freq_relative_interval AS NVARCHAR(10)) + N',' + @CRLF + N' @freq_recurrence_factor = ' + CAST(sch.freq_recurrence_factor AS NVARCHAR(10)) + N',' + @CRLF + N' @active_start_date = ' + CAST(sch.active_start_date AS NVARCHAR(20)) + N',' + @CRLF + N' @active_end_date = ' + CAST(sch.active_end_date AS NVARCHAR(20)) + N',' + @CRLF + N' @active_start_time = ' + CAST(sch.active_start_time AS NVARCHAR(20)) + N',' + @CRLF + N' @active_end_time = ' + CAST(sch.active_end_time AS NVARCHAR(20)) + N';' + @CRLF + @CRLF FROM ( SELECT DISTINCT sch.name, sch.enabled, sch.freq_type, sch.freq_interval, sch.freq_subday_type, sch.freq_subday_interval, sch.freq_relative_interval, sch.freq_recurrence_factor, sch.active_start_date, sch.active_end_date, sch.active_start_time, sch.active_end_time FROM msdb.dbo.sysjobschedules js JOIN msdb.dbo.sysschedules sch ON js.schedule_id = sch.schedule_id WHERE js.job_id = @JobId ) sch ORDER BY sch.name;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
--------------------------------------------------------------------------- -- operator / notify --------------------------------------------------------------------------- SELECT @Tmp = N'EXEC msdb.dbo.sp_update_job' + @CRLF + N' @job_id = @jobId,' + @CRLF + N' @notify_level_email = ' + CAST(j.notify_level_email AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_netsend = ' + CAST(j.notify_level_netsend AS NVARCHAR(10)) + N',' + @CRLF + N' @notify_level_page = ' + CAST(j.notify_level_page AS NVARCHAR(10)) + CASE WHEN ISNULL(@OverrideOperatorName, op_email.name) IS NOT NULL THEN N',' + @CRLF + N' @notify_email_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_email.name), '''', '''''') + N'''' ELSE N'' END + CASE WHEN ISNULL(@OverrideOperatorName, op_netsend.name) IS NOT NULL THEN N',' + @CRLF + N' @notify_netsend_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_netsend.name), '''', '''''') + N'''' ELSE N'' END + CASE WHEN ISNULL(@OverrideOperatorName, op_page.name) IS NOT NULL THEN N',' + @CRLF + N' @notify_page_operator_name = N''' + REPLACE(ISNULL(@OverrideOperatorName, op_page.name), '''', '''''') + N'''' ELSE N'' END + N';' + @CRLF + @CRLF FROM msdb.dbo.sysjobs j LEFT JOIN msdb.dbo.sysoperators op_email ON j.notify_email_operator_id = op_email.id LEFT JOIN msdb.dbo.sysoperators op_netsend ON j.notify_netsend_operator_id = op_netsend.id LEFT JOIN msdb.dbo.sysoperators op_page ON j.notify_page_operator_id = op_page.id WHERE j.job_id = @JobId;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'');
--------------------------------------------------------------------------- -- job server --------------------------------------------------------------------------- IF @AttachToLocalServer = 1 BEGIN SET @JobSQL = @JobSQL + N'EXEC msdb.dbo.sp_add_jobserver' + @CRLF + N' @job_id = @jobId,' + @CRLF + N' @server_name = N''(LOCAL)'';' + @CRLF + @CRLF; END;
--------------------------------------------------------------------------- -- step review --------------------------------------------------------------------------- SET @JobSQL = @JobSQL + N'/* Step Review' + @CRLF;
SET @Tmp = N'';
SELECT @Tmp = @Tmp + N'-- Step ' + CAST(s.step_id AS NVARCHAR(10)) + N' | Name: ' + REPLACE(REPLACE(s.step_name, CHAR(13), N' '), CHAR(10), N' ') + N' | Subsystem: ' + s.subsystem + N' | DB: ' + ISNULL(s.database_name, N'(NULL)') + N' | Proxy: ' + ISNULL(p.name, N'(NULL)') + CASE WHEN s.command LIKE N'%USE [%]%' OR s.command LIKE N'%].dbo.%' OR s.command LIKE N'%.dbo.%' THEN N' | CHECK_COMMAND_FOR_DB_NAME=YES' ELSE N'' END + @CRLF FROM msdb.dbo.sysjobsteps s LEFT JOIN msdb.dbo.sysproxies p ON s.proxy_id = p.proxy_id WHERE s.job_id = @JobId ORDER BY s.step_id;
SET @JobSQL = @JobSQL + ISNULL(@Tmp, N'') + N'*/' + @CRLF + N'GO' + @CRLF + @CRLF;
--------------------------------------------------------------------------- -- 加到總腳本 --------------------------------------------------------------------------- SET @FinalSQL = @FinalSQL + @JobSQL;
FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;END
CLOSE JobCursor;DEALLOCATE JobCursor;
----------------------------------------------------------------------------- 最終輸出---------------------------------------------------------------------------SELECT @FinalSQL AS DeployScript;GOConclusion
The main point of this script is not just export, but to transform Job migration into a “repeatable and verifiable” process.