2497 字
12 分鐘
瀏覽次數
How to Export Multiple SQL Agent Jobs: A Single Script to Recreate Multiple Schedules and Steps
2026-03-24
2026-04-14

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'; -- 指定新 DB
DECLARE @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 可能寫死 DB

Additional 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_name
DECLARE @OverrideOwnerLoginName SYSNAME = N'sa'; -- NULL = 沿用原 owner
DECLARE @OverrideOperatorName SYSNAME = NULL; -- NULL = 沿用原 operator
DECLARE @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_id
FROM @Jobs j
JOIN 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 FOR
SELECT RowNum, SourceJobName, TargetJobName, JobId
FROM @JobList
ORDER BY RowNum;
OPEN JobCursor;
FETCH NEXT FROM JobCursor INTO @RowNum, @SourceJobName, @TargetJobName, @JobId;
WHILE @@FETCH_STATUS = 0
BEGIN
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;
GO

Conclusion#

The main point of this script is not just export, but to transform Job migration into a “repeatable and verifiable” process.

How to Export Multiple SQL Agent Jobs: A Single Script to Recreate Multiple Schedules and Steps
https://joyceowo.github.io/posts/en/32da78ea09fa8006894dd4c158a631fb/
作者
JoyceOwO
發佈於
2026-03-24
許可協議
CC BY-NC-SA 4.0