Home / Databases / SQL Server

DatabasesHow-toIntermediate

Tell me a step failed, but finish the job anyway: step-level failure notifications in SQL Agent

SQL Server Agent notifies per job, not per step, and if a step is set to continue on failure, the job reports success and nobody hears about it. Three patterns for alerting on individual step failures while the rest of the job keeps running.

Deepak Bhardwaj3 min read

Applies toSQL Server 2016–2025, Azure SQL Managed Instance

The problem

A nightly job has six steps: back up five databases, then clean up old files. Step 3 fails, but you set On failure: go to next step so the other backups still run. The job finishes with success, so the job-level failure notification never fires.

Pattern 1: a notify step on the failure path

Insert a notification step after each important step. On failure, route to it. On success, skip it.

Step 1  Backup DB1        success → step 3   failure → step 2
Step 2  Notify DB1 failed success → step 3   failure → step 3
Step 3  Backup DB2        success → step 5   failure → step 4
Step 4  Notify DB2 failed ...

The notify step:

EXEC msdb.dbo.sp_send_dbmail
     @profile_name = N'DBA Mail',
     @recipients   = N'dba-team@example.com',
     @subject      = N'$(ESCAPE_SQUOTE(SRVR)): step failed in job $(ESCAPE_SQUOTE(JOBNAME))',
     @body         = N'A step failed. Check job history for details.';

Agent tokens such as $(ESCAPE_SQUOTE(JOBNAME)) are replaced at run time. Simple, but it doubles the number of steps.

Pattern 2: one final "check" step

Let every step continue on failure, then add a last step that reads the current run's history and fails if any step failed:

DECLARE @job_id uniqueidentifier = CONVERT(uniqueidentifier, $(ESCAPE_SQUOTE(JOBID)));
DECLARE @start datetime = (SELECT MAX(start_execution_date) FROM msdb.dbo.sysjobactivity WHERE job_id = @job_id);

IF EXISTS (
  SELECT 1 FROM msdb.dbo.sysjobhistory h
  WHERE h.job_id = @job_id AND h.step_id > 0 AND h.run_status = 0
    AND msdb.dbo.agent_datetime(h.run_date, h.run_time) >= @start)
  RAISERROR (N'One or more steps failed in this run - see job history.', 16, 1);

Set this step's failure action to Quit the job reporting failure. The job now fails, and the normal job-level notification fires, while every step still ran. This is usually the cleanest pattern.

Warning

Remember to set Quit reporting failure on the check step. If it continues as "success", you're back where you started.

Pattern 3: handle errors inside the step

For T-SQL steps that loop over databases, catch errors per item, log them, and raise once at the end:

DECLARE @errors int = 0, @db sysname;
DECLARE dbs CURSOR LOCAL FAST_FORWARD FOR SELECT name FROM sys.databases WHERE database_id > 4 AND state = 0;
OPEN dbs; FETCH NEXT FROM dbs INTO @db;
WHILE @@FETCH_STATUS = 0
BEGIN
  BEGIN TRY
    DECLARE @sql nvarchar(max) = N'BACKUP DATABASE ' + QUOTENAME(@db) + N' TO DISK = N''B:\Backup\' + @db + N'.bak'' WITH COMPRESSION, CHECKSUM, INIT;';
    EXEC (@sql);
  END TRY
  BEGIN CATCH
    SET @errors += 1;
    PRINT CONCAT(@db, ': ', ERROR_MESSAGE());
  END CATCH
  FETCH NEXT FROM dbs INTO @db;
END
CLOSE dbs; DEALLOCATE dbs;
IF @errors > 0 RAISERROR (N'%d database(s) failed - see step output.', 16, 1, @errors);

Maintenance solutions such as Ola Hallengren's scripts already work this way.

Which to choose

Situation Pattern
Few steps, different people to notify per step 1
Many independent steps, one team 2
One step that loops over many items 3

Prevent it next time

  • Treat "On failure: go to next step" as a design decision that needs a final check step.
  • Review jobs whose steps continue on failure:
SELECT j.name, s.step_id, s.step_name
FROM msdb.dbo.sysjobsteps s JOIN msdb.dbo.sysjobs j ON j.job_id = s.job_id
WHERE s.on_fail_action IN (3, 4);   -- go to next step / go to step n