Writes, optimizes, and debugs T-SQL queries. Explains SQL Server internals, troubleshoots performance issues, and guides database administration tasks including backup/restore, high availability, security, and index design. Use when the user asks about T-SQL syntax, SQL Server administration, query performance, stored procedures, indexes, locking, transactions, backup/restore, high availability, security, or any MSSQL-related topic — even without saying 'SQL Server' explicitly. Also trigger on terms like SSMS, tempdb, bcp, sqlcmd, MSSQL, sp_executesql, NOLOCK, columnstore, Hekaton, RCSI, param sniffing, or execution plan.
100
100%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
SQL Server Agent is the built-in job scheduler and automation engine for SQL Server. It runs as a Windows service (SQLSERVERAGENT) and handles scheduled jobs, event-driven alerts, operator notifications, and multi-server administration. Almost everything in production SQL Server operations — backups, index maintenance, statistics updates, monitoring — relies on SQL Server Agent.
SQL Server Agent is the right tool for:
Use SSIS, Azure Data Factory, or an external scheduler (Airflow, Quartz) when you need complex dependencies, parallel step execution, or cross-platform scheduling. Agent is excellent for SQL-centric, sequential automation.
SQL Server Agent Service (SQLSERVERAGENT)
│
├── Jobs ─────── Steps (T-SQL, SSIS, PowerShell, CmdExec, …)
│ └── Schedules (one-time, recurring, on-startup, on-idle)
│
├── Alerts ────── Error number / Severity / Performance condition / WMI event
│ └── → Response: run job, notify operators
│
├── Operators ─── Email / Net Send (deprecated) / Pager (deprecated)
│
└── Proxies ────── Credential-backed Windows identity for non-sysadmin stepsAgent stores all metadata in msdb — jobs, history, schedules, alerts, operators, and proxies all live there.
Agent runs as a Windows service and must be running for any automation to work. Agent is separate from the SQL Server service itself — a SQL Server instance can be up while Agent is down.
[!WARNING] Azure SQL Database SQL Server Agent is not available on Azure SQL Database. Use Elastic Jobs or Azure Automation instead. SQL Managed Instance does support Agent with full T-SQL compatibility.
| Concept | Description |
|---|---|
| Job | Named container for one or more steps + one or more schedules |
| Step | A single unit of work: a T-SQL batch, SSIS package, shell command, etc. |
| Schedule | When the job runs: one-time, recurring (cron-like), on startup, on idle |
| Alert | Event trigger: SQL error, severity, performance counter, WMI |
| Operator | Named person/group to notify (email address etc.) |
| Proxy | Windows credential used by non-sysadmin job steps to run under a specific identity |
| Job category | Optional grouping label stored in msdb.dbo.syscategories |
A job can have multiple schedules. A schedule can be shared across multiple jobs. Steps can branch: on success go to step N, on failure go to step M, or quit with success/failure.
Complete example: a nightly index maintenance job.
USE msdb;
GO
-- 1. Create the job
EXEC sp_add_job
@job_name = N'Nightly Index Maintenance',
@description = N'Rebuild/reorganize fragmented indexes using Ola Hallengren IndexOptimize',
@category_name = N'Database Maintenance',
@owner_login_name = N'sa', -- or a service account login
@enabled = 1,
@notify_level_email = 2, -- 1=success, 2=failure, 3=always
@notify_email_operator_name = N'DBA Team';
-- 2. Add a step
EXEC sp_add_jobstep
@job_name = N'Nightly Index Maintenance',
@step_name = N'Run IndexOptimize',
@step_id = 1,
@subsystem = N'TSQL',
@command = N'EXEC master.dbo.IndexOptimize
@Databases = ''USER_DATABASES'',
@FragmentationLow = NULL,
@FragmentationMedium = ''INDEX_REORGANIZE'',
@FragmentationHigh = ''INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE'',
@FragmentationLevel1 = 5,
@FragmentationLevel2 = 30,
@LogToTable = ''Y'';',
@database_name = N'master',
@on_success_action = 1, -- 1=quit with success
@on_fail_action = 2, -- 2=quit with failure
@retry_attempts = 0,
@retry_interval = 0;
-- 3. Add a schedule (every night at 02:00)
EXEC sp_add_schedule
@schedule_name = N'Nightly 2AM',
@freq_type = 4, -- 4=daily
@freq_interval = 1, -- every 1 day
@active_start_time = 020000; -- 02:00:00 (HHMMSS integer)
-- 4. Attach the schedule to the job
EXEC sp_attach_schedule
@job_name = N'Nightly Index Maintenance',
@schedule_name = N'Nightly 2AM';
-- 5. Target the local server
EXEC sp_add_jobserver
@job_name = N'Nightly Index Maintenance',
@server_name = N'(LOCAL)';
GORun a job immediately (for testing):
EXEC msdb.dbo.sp_start_job @job_name = N'Nightly Index Maintenance';Stop a running job:
EXEC msdb.dbo.sp_stop_job @job_name = N'Nightly Index Maintenance';Delete a job:
EXEC msdb.dbo.sp_delete_job @job_name = N'Nightly Index Maintenance';The @subsystem parameter controls what runs in each step.
| Subsystem | Value | Description | Notes |
|---|---|---|---|
| T-SQL | TSQL | T-SQL batch | Runs under job owner or proxy; @database_name sets USE context |
| SSIS | SSIS | SQL Server Integration Services package | Requires SSIS installed; use proxy for non-sysadmin |
| PowerShell | PowerShell | PowerShell script | Agent runs 32-bit PowerShell by default on older versions |
| CmdExec | CmdExec | Windows command shell (cmd.exe) | Requires proxy for non-sysadmin steps |
| ActiveX Script | ActiveScripting | VBScript / JScript | Deprecated — removed in SQL Server 2016 |
| Analysis Services Command | ANALYSISCOMMAND | XMLA against SSAS | Requires SSAS |
| Analysis Services Query | ANALYSISQUERY | MDX against SSAS | Requires SSAS |
| Replication Distributor | DISTRIBUTION | Replication distribution agent | Internal use |
| Replication Log Reader | LOGREADER | Replication log reader agent | Internal use |
| Replication Merge | MERGE | Replication merge agent | Internal use |
| Replication Queue Reader | QUEUEREADER | Replication queue reader | Internal use |
| Replication Snapshot | SNAPSHOT | Replication snapshot agent | Internal use |
[!WARNING] ActiveX Script subsystem Removed in SQL Server 2016. Migrate to PowerShell or CmdExec + scripts.
@database_name sets the initial USE context — defaults to msdb if unset, which is almost never correct@@ERROR and RAISERROR do not cause a step failure unless they result in an unhandled error with severity ≥ 11IF @@ERROR <> 0 RAISERROR(...) or SET XACT_ABORT ON + TRY/CATCH with THROW to propagate failures reliablySQLPS is auto-imported (older versions); on newer SQL Server the SqlServer module must be imported explicitly$LASTEXITCODE and exit 1 control step outcomesqlcmd, bcp, .bat files, Python scripts, etc.Each step has independent success and failure actions:
@on_success_action / @on_fail_action value | Meaning |
|---|---|
1 | Quit the job reporting success |
2 | Quit the job reporting failure |
3 | Go to the next step |
4 | Go to step N (set @on_success_step_id / @on_fail_step_id) |
Example multi-step job with conditional branching:
-- Step 1: Extract
EXEC sp_add_jobstep
@job_name = N'ETL Pipeline',
@step_name = N'Extract',
@step_id = 1,
@subsystem = N'TSQL',
@command = N'EXEC dbo.usp_Extract;',
@database_name = N'Staging',
@on_success_action = 3, -- go to next step
@on_fail_action = 4, -- go to step 3 (cleanup)
@on_fail_step_id = 3;
-- Step 2: Load
EXEC sp_add_jobstep
@job_name = N'ETL Pipeline',
@step_name = N'Load',
@step_id = 2,
@subsystem = N'TSQL',
@command = N'EXEC dbo.usp_Load;',
@database_name = N'DWH',
@on_success_action = 1, -- quit success
@on_fail_action = 4, -- go to step 3 (cleanup)
@on_fail_step_id = 3;
-- Step 3: Cleanup / rollback (always runs on failure path)
EXEC sp_add_jobstep
@job_name = N'ETL Pipeline',
@step_name = N'Cleanup on Failure',
@step_id = 3,
@subsystem = N'TSQL',
@command = N'EXEC dbo.usp_CleanupStagingOnFailure;',
@database_name = N'Staging',
@on_success_action = 2, -- quit failure (so job is marked failed)
@on_fail_action = 2;@freq_type values| Value | Meaning |
|---|---|
1 | One time only |
4 | Daily |
8 | Weekly |
16 | Monthly |
32 | Monthly, relative (e.g., "first Monday") |
64 | When Agent starts |
128 | When CPU is idle |
@freq_interval semantics by freq_type@freq_type | @freq_interval meaning |
|---|---|
| 4 (daily) | Every N days (1 = every day) |
| 8 (weekly) | Bitmask: 1=Sun, 2=Mon, 4=Tue, 8=Wed, 16=Thu, 32=Fri, 64=Sat; combine with ` |
| 16 (monthly) | Day of month (1–31) |
| 32 (monthly relative) | 1=Sun, 2=Mon, 3=Tue, 4=Wed, 5=Thu, 6=Fri, 7=Sat, 8=Day, 9=Weekday, 10=Weekend day |
For jobs running every N minutes/hours within a window:
EXEC sp_add_schedule
@schedule_name = N'Every 15 min business hours',
@freq_type = 4, -- daily
@freq_interval = 1,
@freq_subday_type = 4, -- 4=minutes, 8=hours
@freq_subday_interval = 15,
@active_start_time = 080000, -- 08:00:00
@active_end_time = 180000; -- 18:00:00@freq_subday_type values: 1 = once (no subday), 2 = seconds, 4 = minutes, 8 = hours.
Schedules can be detached and attached independently:
-- Detach a schedule from a job (schedule still exists)
EXEC sp_detach_schedule
@job_name = N'Some Job',
@schedule_name = N'Nightly 2AM';
-- Delete a schedule only if no jobs use it
EXEC sp_delete_schedule
@schedule_name = N'Nightly 2AM',
@keep_unused_schedules = 0;Alerts fire in response to events. They can run a job, notify operators, or both.
@event_category_name / mechanism | Trigger |
|---|---|
| SQL Server error alert | Specific error number occurs in the SQL error log |
| SQL Server event alert | Error of a given severity (e.g., all severity 16) |
| SQL Server performance condition | PerfMon counter crosses a threshold |
| WMI event | WMI event query fires |
EXEC msdb.dbo.sp_add_alert
@name = N'Alert: Error 823 (I/O Error)',
@message_id = 823, -- specific error number
@severity = 0, -- 0 = use message_id, not severity
@enabled = 1,
@delay_between_responses = 900, -- 15 min minimum between firings (seconds)
@notification_message = N'Disk I/O error 823 detected',
@job_name = N'', -- optionally run a job
@raise_snmp_trap = 0;EXEC msdb.dbo.sp_add_alert
@name = N'Alert: Severity 16-19',
@message_id = 0, -- 0 = use severity, not message_id
@severity = 16, -- fires on severity 16 OR HIGHER up to 18
@enabled = 1,
@delay_between_responses = 300;[!NOTE] Severities 20–25 are fatal and terminate the connection. They are still alertable but you need
WITH LOGon the error for Agent to see it.
EXEC msdb.dbo.sp_add_alert
@name = N'Alert: Low PLE',
@message_id = 0,
@severity = 0,
@enabled = 1,
@performance_condition = N'SQLServer:Buffer Manager|Page life expectancy||<|300',
-- format: object|counter|instance|comparator|value
@delay_between_responses = 300;Common performance condition alert targets:
| Object | Counter | Threshold |
|---|---|---|
SQLServer:Buffer Manager | Page life expectancy | < 300 |
SQLServer:SQL Statistics | Batch Requests/sec | > 5000 |
SQLServer:General Statistics | Processes blocked | > 5 |
SQLServer:Locks | Lock Waits/sec | > 100 |
SQLServer:Memory Manager | Memory Grants Pending | > 0 |
EXEC msdb.dbo.sp_add_notification
@alert_name = N'Alert: Error 823 (I/O Error)',
@operator_name = N'DBA Team',
@notification_method = 1; -- 1=email, 2=pager, 4=net send; combine as bitmaskOperators are named recipients for notifications. Email is the only practical notification method in modern SQL Server (pager/net send are effectively deprecated).
-- Requires Database Mail to be configured first
EXEC msdb.dbo.sp_add_operator
@name = N'DBA Team',
@enabled = 1,
@email_address = N'dba-team@company.com',
@weekday_pager_start_time = 090000, -- ignored if no pager configured
@weekday_pager_end_time = 180000,
@pager_days = 62; -- Mon-Fri bitmask (2+4+8+16+32)
-- Set a "fail-safe" operator: notified if Agent can't contact normal operators
EXEC msdb.dbo.sp_update_agent_parameter
@param_name = N'FailSafeOperator',
@param_value = N'DBA Team';Delete an operator:
EXEC msdb.dbo.sp_delete_operator @name = N'DBA Team';By default, T-SQL steps run as the job owner. For CmdExec, SSIS, and PowerShell steps, non-sysadmin owners need a proxy.
A proxy maps a Windows credential (stored in sys.credentials) to a set of subsystems and grants specific principals access to use it.
-- 1. Create a Windows credential
CREATE CREDENTIAL [CORP\svc-sqlagent]
WITH IDENTITY = N'CORP\svc-sqlagent',
SECRET = N'<windows-password>';
-- 2. Create a proxy backed by that credential
EXEC msdb.dbo.sp_add_proxy
@proxy_name = N'SSIS Proxy',
@credential_name = N'CORP\svc-sqlagent',
@enabled = 1;
-- 3. Grant the proxy permission to a subsystem
EXEC msdb.dbo.sp_grant_proxy_to_subsystem
@proxy_name = N'SSIS Proxy',
@subsystem_id = 11; -- 11=SSIS; see subsystem IDs below
-- 4. Grant a login the right to use the proxy
EXEC msdb.dbo.sp_grant_login_to_proxy
@login_name = N'CORP\developer1',
@proxy_name = N'SSIS Proxy';| ID | Subsystem |
|---|---|
| 1 | ActiveX Script (deprecated) |
| 2 | CmdExec |
| 3 | Distribution (Replication) |
| 4 | Snapshot (Replication) |
| 5 | Log Reader (Replication) |
| 6 | Merge (Replication) |
| 7 | Queue Reader (Replication) |
| 8 | Analysis Services Command |
| 9 | Analysis Services Query |
| 11 | SSIS |
| 12 | PowerShell |
sysadmin members can run any step as any identity, no proxy requiredMulti-Server Administration (MSX/TSX) lets you manage jobs centrally from a Master Server (MSX) and push them to Target Servers (TSX).
-- On the master server: make it an MSX
EXEC msdb.dbo.sp_msx_set_account @credential_name = N'MSX Credential';
-- On a target server: enlist it in the MSX
EXEC msdb.dbo.sp_msx_enlist
@msx_server_name = N'SQLMASTERSVR',
@location = N'Datacenter East';
-- On the master: create a multi-server job (targets all enlisted servers)
EXEC msdb.dbo.sp_add_jobserver
@job_name = N'Nightly Backups',
@server_name = N'ALL'; -- sends to all enlisted target servers[!NOTE] MSX/TSX is a legacy feature. For large environments, consider Ola Hallengren's scripts with a central management server, or third-party tools (DBA MultiTool, dbatools) for distributed job management.
| Table | Contents |
|---|---|
msdb.dbo.sysjobs | Job definitions (name, enabled, description, owner) |
msdb.dbo.sysjobsteps | Step definitions (command, subsystem, database, retry settings) |
msdb.dbo.sysjobschedules | Link table between jobs and schedules |
msdb.dbo.sysschedules | Schedule definitions (freq_type, freq_interval, active times) |
msdb.dbo.sysjobhistory | Execution history (run_status, run_duration, message) |
msdb.dbo.sysjobactivity | Real-time job activity (start_execution_date, stop_execution_date) |
msdb.dbo.sysalerts | Alert definitions |
msdb.dbo.sysoperators | Operator definitions |
msdb.dbo.sysnotifications | Alert → operator notification links |
msdb.dbo.sysproxies | Proxy definitions |
msdb.dbo.syscredentials | Credential-to-proxy mapping |
msdb.dbo.sysjobservers | Job-to-server assignment (for multi-server) |
msdb.dbo.syscategories | Job/alert/operator categories |
SELECT
j.name AS job_name,
h.step_id,
h.step_name,
CASE h.run_status
WHEN 0 THEN 'Failed'
WHEN 1 THEN 'Succeeded'
WHEN 2 THEN 'Retry'
WHEN 3 THEN 'Cancelled'
WHEN 4 THEN 'Running'
END AS status,
-- Convert YYYYMMDD + HHMMSS integers to a datetime
CONVERT(datetime,
STUFF(STUFF(CAST(h.run_date AS varchar(8)), 7, 0, '-'), 5, 0, '-')
+ ' '
+ STUFF(STUFF(RIGHT('000000' + CAST(h.run_time AS varchar(6)), 6), 5, 0, ':'), 3, 0, ':')
) AS run_start,
-- run_duration is HHMMSS integer
(h.run_duration / 10000 * 3600)
+ ((h.run_duration % 10000) / 100 * 60)
+ (h.run_duration % 100) AS duration_seconds,
LEFT(h.message, 500) AS message
FROM msdb.dbo.sysjobhistory h
JOIN msdb.dbo.sysjobs j ON j.job_id = h.job_id
WHERE h.run_date >= CONVERT(int, CONVERT(varchar(8), DATEADD(day, -7, GETDATE()), 112))
ORDER BY h.run_date DESC, h.run_time DESC;SELECT
j.name AS job_name,
h.step_name,
CONVERT(datetime,
STUFF(STUFF(CAST(h.run_date AS varchar(8)), 7, 0, '-'), 5, 0, '-')
+ ' '
+ STUFF(STUFF(RIGHT('000000' + CAST(h.run_time AS varchar(6)), 6), 5, 0, ':'), 3, 0, ':')
) AS run_start,
LEFT(h.message, 500) AS failure_message
FROM msdb.dbo.sysjobhistory h
JOIN msdb.dbo.sysjobs j ON j.job_id = h.job_id
WHERE h.run_status = 0 -- failed
AND h.step_id > 0 -- exclude job-level summary row (step_id=0)
AND h.run_date >= CONVERT(int, CONVERT(varchar(8), DATEADD(hour, -24, GETDATE()), 112))
ORDER BY h.run_date DESC, h.run_time DESC;SELECT
j.name AS job_name,
a.start_execution_date,
DATEDIFF(second, a.start_execution_date, GETDATE()) AS running_seconds,
ja.last_executed_step_id,
ja.last_executed_step_date
FROM msdb.dbo.sysjobactivity a
JOIN msdb.dbo.sysjobs j ON j.job_id = a.job_id
LEFT JOIN (
SELECT job_id, last_executed_step_id = step_id,
last_executed_step_date = MAX(run_requested_date)
FROM msdb.dbo.sysjobhistory
GROUP BY job_id, step_id
) ja ON ja.job_id = j.job_id
WHERE a.session_id = (
SELECT MAX(session_id) FROM msdb.dbo.syssessions
)
AND a.start_execution_date IS NOT NULL
AND a.stop_execution_date IS NULL;SELECT
j.name AS job_name,
j.enabled,
s.name AS schedule_name,
s.enabled AS schedule_enabled,
CASE s.freq_type
WHEN 1 THEN 'One time'
WHEN 4 THEN 'Daily every ' + CAST(s.freq_interval AS varchar) + ' day(s)'
WHEN 8 THEN 'Weekly'
WHEN 16 THEN 'Monthly day ' + CAST(s.freq_interval AS varchar)
WHEN 32 THEN 'Monthly relative'
WHEN 64 THEN 'Agent start'
WHEN 128 THEN 'CPU idle'
END AS frequency,
s.active_start_time
FROM msdb.dbo.sysjobs j
JOIN msdb.dbo.sysjobschedules js ON js.job_id = j.job_id
JOIN msdb.dbo.sysschedules s ON s.schedule_id = js.schedule_id
ORDER BY j.name;WITH LastRun AS (
SELECT job_id,
run_status,
run_date,
run_time,
message,
ROW_NUMBER() OVER (PARTITION BY job_id ORDER BY run_date DESC, run_time DESC) AS rn
FROM msdb.dbo.sysjobhistory
WHERE step_id = 0 -- job-level summary only
)
SELECT
j.name AS job_name,
j.enabled,
CASE lr.run_status
WHEN 0 THEN 'Failed'
WHEN 1 THEN 'Succeeded'
WHEN 2 THEN 'Retry'
WHEN 3 THEN 'Cancelled'
ELSE 'Unknown'
END AS last_status,
CONVERT(datetime,
STUFF(STUFF(CAST(lr.run_date AS varchar(8)), 7, 0, '-'), 5, 0, '-')
+ ' '
+ STUFF(STUFF(RIGHT('000000' + CAST(lr.run_time AS varchar(6)), 6), 5, 0, ':'), 3, 0, ':')
) AS last_run,
LEFT(lr.message, 200) AS message
FROM msdb.dbo.sysjobs j
LEFT JOIN LastRun lr ON lr.job_id = j.job_id AND lr.rn = 1
WHERE j.enabled = 1
ORDER BY j.name;-- If this returns a row, Agent is running
SELECT *
FROM sys.dm_server_services
WHERE servicename LIKE N'SQL Server Agent%';EXEC msdb.dbo.sp_cycle_agent_errorlog; -- rotate the log (new log file)
-- Read Agent error log (no T-SQL equivalent — read from filesystem or SSMS)
-- Log path: same directory as SQL Server error log, named SQLAGENT.OUT (plus numbered rollover files)SELECT
a.name AS alert_name,
a.occurrence_count,
a.last_occurrence_date,
a.last_occurrence_time,
a.last_response_date,
a.last_response_time
FROM msdb.dbo.sysalerts a
ORDER BY a.last_occurrence_date DESC, a.last_occurrence_time DESC;Agent history is finite — old rows are purged when the count exceeds the limit.
-- View current history settings
EXEC msdb.dbo.sp_get_composite_job_info;
-- Change history limits (max rows per job and overall)
EXEC msdb.dbo.sp_set_sqlagent_properties
@jobhistory_max_rows = 100000, -- default: 1000 total
@jobhistory_max_rows_per_job = 1000; -- default: 100 per job[!WARNING] Default history limits (1000 total / 100 per job) are far too low for production. Increase them immediately or use a custom job history table.
SQL Server Agent has limited awareness of T-SQL errors inside steps. These patterns make step failure detection reliable:
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- ... work ...
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
THROW; -- re-raise: non-zero exit → step fails
END CATCH;Any unhandled error with severity 11–25 causes the step to fail:
IF NOT EXISTS (SELECT 1 FROM dbo.SomeTable WHERE ...)
BEGIN
RAISERROR('Prerequisite check failed: no rows found in SomeTable', 16, 1);
RETURN;
END;Agent does not inspect stored procedure return values automatically. Wrap calls:
DECLARE @rc int;
EXEC @rc = dbo.usp_SomeProc;
IF @rc <> 0
RAISERROR('usp_SomeProc failed with return code %d', 16, 1, @rc);Use step routing (see Step Success/Failure Routing) to route failures to a cleanup step that logs the error and returns exit code 2 (fail) so the job is marked failed.
| Feature | Azure SQL Database | Azure SQL Managed Instance |
|---|---|---|
| SQL Server Agent | Not available | Full support |
T-SQL job management (sp_add_job) | Not available | Same as on-prem |
| Elastic Jobs | GA — use instead of Agent | Not needed |
| Azure Automation | Can call Azure SQL | Can call MI |
| WMI alerts | Not available | Available |
| Performance condition alerts | Not available | Available |
| Database Mail | Not available on DB | Available on MI |
-- Create elastic job agent (done once per resource group in Azure Portal or ARM)
-- Then use elastic jobs T-SQL (different stored procs than Agent):
EXEC jobs.sp_add_job @job_name = N'Nightly Maintenance';
EXEC jobs.sp_add_jobstep
@job_name = N'Nightly Maintenance',
@command = N'UPDATE STATISTICS dbo.Orders;',
@target_group_name = N'All Prod Databases';
EXEC jobs.sp_start_job @job_name = N'Nightly Maintenance';CREATE OR ALTER PROCEDURE dbo.usp_NotifyJobFailure
@JobName nvarchar(128),
@StepName nvarchar(128),
@ErrorMsg nvarchar(2048)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @body nvarchar(4000);
SET @body = N'<h3>Job Failure Alert</h3>'
+ N'<b>Server:</b> ' + @@SERVERNAME + N'<br>'
+ N'<b>Job:</b> ' + @JobName + N'<br>'
+ N'<b>Step:</b> ' + @StepName + N'<br>'
+ N'<b>Time:</b> ' + CONVERT(varchar(25), GETDATE(), 120) + N'<br>'
+ N'<b>Error:</b> ' + @ErrorMsg;
EXEC msdb.dbo.sp_send_dbmail
@profile_name = N'Default',
@recipients = N'dba-team@company.com',
@subject = N'SQL Agent Job Failed: ' + @JobName,
@body = @body,
@body_format = N'HTML';
END;-- Disable all backup jobs during migration
UPDATE msdb.dbo.sysjobs
SET enabled = 0
WHERE name LIKE N'%Backup%';
-- Re-enable afterwards
UPDATE msdb.dbo.sysjobs
SET enabled = 1
WHERE name LIKE N'%Backup%';[!WARNING] Direct table updates to
msdbare generally unsupported. Prefersp_update_job @enabled = 0for single jobs. For bulk operations, consider a cursor oversp_update_jobor usedbatools(Disable-DbaAgentJob).
CREATE TRIGGER trg_AuditJobChanges
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
BEGIN
-- Agent changes fire through msdb stored procedures, not DDL events
-- Use the sysjobs_view change_date column instead:
-- SELECT * FROM msdb.dbo.sysjobs ORDER BY date_modified DESC
END;Agent job changes do not fire DDL triggers. Track changes using sysjobs.date_modified or sp_help_jobhistory.
IF NOT EXISTS (
SELECT 1 FROM msdb.dbo.sysjobs WHERE name = N'My Job'
)
BEGIN
EXEC msdb.dbo.sp_add_job @job_name = N'My Job', ...;
END;SELECT
j.name,
j.enabled,
SUSER_SNAME(j.owner_sid) AS owner
FROM msdb.dbo.sysjobs j
WHERE SUSER_SNAME(j.owner_sid) = N'CORP\svc-account'
ORDER BY j.name;# Copy all Agent jobs from one server to another
Copy-DbaAgentJob -Source SQLOLD -Destination SQLNEW
# Export all jobs to JSON files for source control
Export-DbaAgentJob -SqlInstance SQLPROD -Path C:\AgentJobs\Agent must be running — SQL Server can be up while Agent is stopped. Jobs will silently not run. Set Agent to Automatic start in Windows Services.
Default history limits are tiny — 1000 total rows / 100 per job means active servers lose history within hours. Increase with sp_set_sqlagent_properties.
Job owner SID mismatch after restore — When msdb is restored to a new server, job owner SIDs may not match logins. Run EXEC msdb.dbo.sp_update_job @job_name = ..., @owner_login_name = ... to re-map owners.
@database_name defaults to msdb — If you omit @database_name in sp_add_jobstep, T-SQL steps run in the context of msdb, not your application database. Always specify it.
T-SQL step failures require severity ≥ 11 — A PRINT statement or RAISERROR with severity ≤ 10 does not fail a step. You must raise with severity 11 or higher, or the step succeeds even when your business logic detected an error.
SSIS steps require matching bitness — If the SSIS package uses 32-bit drivers, you may need the 32-bit runtime. SQL Server Agent runs 64-bit by default. Configure the step to use 32-bit via the package properties.
Non-sysadmin owners of CmdExec/SSIS steps need a proxy — Without a proxy, Agent will fail the step with "Access is denied". Sysadmin members bypass this requirement — which is why Agent service accounts are often (dangerously) sysadmin.
Alerts don't fire for PRINT output — Alerts fire on errors written to the SQL Server error log. Only errors raised with WITH LOG or severity ≥ 17 go to the error log automatically. Use RAISERROR(...) WITH LOG to guarantee alert firing.
delay_between_responses is critical for noisy alerts — Without a delay, a flooding error can generate thousands of emails per hour. Set @delay_between_responses to at least 300 seconds (5 min) for most alerts.
Run-time of run_duration is HHMMSS integer — run_duration = 10230 means 1 hour, 2 minutes, 30 seconds — not 10,230 seconds. Use the conversion formula in the queries above.
Multi-server jobs: target server must be enlisted first — Sending a job to N'ALL' only reaches currently enlisted target servers. New servers added later need the job pushed to them explicitly.
Job steps run in their own SPID — There is no implicit transaction across steps. Each step starts fresh. If step 1 creates a temp table and step 2 needs it, use a real table (permanent or ##global_temp), not #local_temp.
48-database-mail.md — Required for email notifications from alerts and jobs49-configuration-tuning.md — sp_configure, Resource Governor (for throttling Agent workloads)44-backup-restore.md — Ola Hallengren backup jobs (the most common Agent job pattern)38-auditing.md — SQL Server Audit (alternative/complement to Agent-driven alerts)references