SQL Agent Job Failed: Finding the Step That Actually Broke

⚡“The job failed” is not the error. It is the summary row. SQL Server Agent writes 2 kinds of row to job history: the job outcome row, which is step_id = 0 and never carries a cause, and 1 row per step that ran, which does. Read the step row first. After that, 5 messages cover almost every production failure: an owner the server can no longer resolve, a proxy whose credential will not log on, a permission the step context does not have, a path the Agent service account cannot see, and a message that was cut off before it reached the point.

What the Message Says, and Where to Start

Find the line you are actually looking at. Some of these come from the job outcome row and some from the step, and they send you to different places.

What job history saysWhat it means
All you have is the job outcome row Step 1: read the step row
The job failed. The Job was invoked by User x. The last step to run was step nNames the last step, never the cause
The owner no longer resolves Step 2: the job owner
Unable to determine if the owner (x) of job y has server accessThe owner SID matches no login, error 15404
The step never started Step 3: proxy and credential
Unable to start execution of step n (reason: Error authenticating proxy x)The credential could not log on, so no command was issued
The step ran and was refused Step 4: the context it ran as
Executed as user: x. The SELECT permission was denied ... (229)The context in the prefix lacks the right, not you
The step could not reach the path Step 5: paths and the Agent account
Cannot open backup device ... Operating system error 3 or 53 is not there from the service, 5 is there and refused
The message is cut off Step 6: truncated messages
The message ends in the middle of a sentenceHistory keeps about 2,000 characters of step output

If the job is one of the DBA % maintenance jobs, the quiet-failure section below is the faster route, because those fail in a specific way and there is already a script that reads their history for you.


STEP1

Step 1: Read the Step Row, Not the Job Row

Everything you need is in msdb.dbo.sysjobhistory, and the row most people read is the wrong one. A run writes 1 row per step plus 1 outcome row, and the outcome row is step_id = 0. That row exists to record that the run ended and which step it ended on. It has never carried an error message. The step rows do, so filter them in.

SELECT TOP (20)
       j.name                                           AS job_name,
       h.step_id,
       h.step_name,
       msdb.dbo.agent_datetime(h.run_date, h.run_time)  AS run_at,
       h.run_duration,
       h.message
FROM   msdb.dbo.sysjobhistory AS h
JOIN   msdb.dbo.sysjobs       AS j ON j.job_id = h.job_id
WHERE  h.run_status = 0        -- 0 = Failed, 1 = Succeeded, 2 = Retry, 3 = Cancelled
  AND  h.step_id   > 0         -- drop the job outcome row, it has no cause in it
ORDER  BY h.instance_id DESC;

The difference is not subtle. These 2 rows are the same run of the same job, the outcome row first and the step row second.

The same failure, both rowsjob history output, read it, do not run it
step_id message ——- —————————————————————————- 0 The job failed. The Job was invoked by User HPAI01\Peter. The last step to run was step 1 (Step 1 Select). 1 Executed as user: Lab1001_owner. The SELECT permission was denied on the object ‘Secret’, database ‘Lab1001_jobs’, schema ‘dbo’. [SQLSTATE 42000] (Error 229). The step failed.

Note the Executed as user: prefix on the step row. That is the security context the step actually ran in, and it is the single most useful thing on the line, because it is often not the context you assumed. Get SQL Agent Job Failure Summary runs this across the whole instance for the last 7 days, which is the version worth keeping, because job history is only useful when you read it without having to go looking. The table itself is documented in MS Docs for sysjobhistory.


STEP2

Step 2: The Owner the Server Can No Longer Resolve

This one is unusual, because the failure is recorded on the job outcome row rather than on a step. There is no step error at all, since no step was allowed to start.

Job outcome row, owner cannot be resolvedjob history output, read it, do not run it
The job failed. Unable to determine if the owner (Lab1001_owner) of job Lab1001_OwnerGone has server access.

A job stores its owner as a SID in msdb.dbo.sysjobs.owner_sid, not as a name. When that SID stops resolving, usually because the Windows or domain account behind it was deleted or renamed, or because Agent cannot reach a domain controller to ask, Agent cannot establish whether the owner is still allowed to run anything, and it refuses rather than guessing. The underlying message is 15404, severity 16, and on the instance it reads Could not obtain information about Windows NT group/user '%ls', error code %#lx.

Find every job in that state in 1 query, no job name needed:

SELECT j.name,
       j.owner_sid,
       SUSER_SNAME(j.owner_sid) AS owner_login   -- NULL means it no longer resolves
FROM   msdb.dbo.sysjobs AS j
WHERE  SUSER_SNAME(j.owner_sid) IS NULL;

The fix is to reassign the owner, with EXEC msdb.dbo.sp_update_job @job_name = N'...', @owner_login_name = N'sa'; or, better, a dedicated service login that nobody is going to decommission. Worth knowing: a plain DROP LOGIN will not create this state, because SQL Server blocks it with Msg 15170 ... This login is the owner of 1 job(s). You must delete or reassign these jobs before the login can be dropped. The orphan almost always arrives from the Windows side, where nothing consults SQL Server first. Get Login and Job Inventory lists every job with its owner so you can see which ones hang off a human account, and SQL Server Login Migration: What Gets Silently Left Behind is the same failure arriving a week after a server move.


STEP3

Step 3: The Proxy, and the Credential Behind It

If the message begins Unable to start execution of step, the step never ran. There is no command output to read because no command was issued, and chasing the T-SQL or the batch file is wasted time.

Step row, proxy authenticationjob history output, read it, do not run it
Unable to start execution of step 1 (reason: Error authenticating proxy HPAI01\Guest, system error: The user name or password is incorrect.). The step failed.

A proxy is a name over a credential, and the credential holds a Windows account and a password. The password is a copy. When that account’s password is rotated in Active Directory, nothing updates the credential, so every job step that runs under the proxy starts failing at the same moment, which is the giveaway: several unrelated jobs breaking on the same night usually means 1 credential, not several bugs. Repoint it with ALTER CREDENTIAL and the current password, then run 1 step by hand to confirm. The proxy model itself is in MS Docs on creating an Agent proxy.

Get Audit Specifications, DDL Triggers, and Proxy Credentials lists every proxy with the credential and identity behind it, which is also the review you want for a different reason: a proxy lets a job step run under an account more privileged than the login that owns the job.


STEP4

Step 4: The Permission the Step Actually Runs With

Read the Executed as user: prefix before you read the error after it. A T-SQL step in a job owned by a sysadmin runs as the SQL Server Agent service account, which is the NT SERVICE\SQLSERVERAGENT prefix on the capture in Step 5. A T-SQL step in a job owned by anyone else runs as that owner, which is the Lab1001_owner prefix on the capture in Step 1. Both of those came off the same instance, on consecutive runs, and the only thing that differed was who owned the job.

That is why “it works when I run it in SSMS” proves nothing. In SSMS the batch runs as you, and you are probably a sysadmin. Grant the right to the context named in the message, not to yourself, then re-run the step. The SELECT Permission Was Denied on the Object (Error 229) decodes that family, including the cases where the object is a view and the permission you need is on something else.

There is a sharper version of the same fault. With the owner login present but with no user in the step’s target database, the job did not reach the command at all, and the step row read 'EXECUTE AS LOGIN' failed for the requested login 'Lab1001_owner'. The step failed. Creating the database user, with no permissions granted at all, moved the failure forward to the 229 above. So a step that fails before it runs anything is usually a mapping problem, and a step that fails with a permission error has already got past that.


STEP5

Step 5: The Path the Agent Account Cannot See

Backup, restore, BULK INSERT and CmdExec steps all fail this way, and the message is generous: it names the account and the exact path it tried.

Step row, backup pathjob history output, read it, do not run it
Executed as user: NT SERVICE\SQLSERVERAGENT. Cannot open backup device ‘D:\Lab1001_NoSuchFolder\Lab1001_jobs.bak’. Operating system error 3(The system cannot find the path specified.). [SQLSTATE 42000] (Error 3201) BACKUP DATABASE is terminating abnormally. [SQLSTATE 42000] (Error 3013). The step failed.

Operating system error 3 and operating system error 5 are 2 different jobs of work. Error 3 means the path is not there from where the service is standing: a folder that was removed, or a drive letter that only exists in somebody’s interactive session. A mapped drive is the classic one, because a drive letter mapped by a logged-on user does not exist in a service session at all, so a backup path of Z:\SQL\ can work perfectly when you test it and fail every night. Error 5 means the path is there and the account is refused, which is a share permission, an NTFS permission, or both. Cannot Open Backup Device: Operating System Error 5 is the full walkthrough for the access-denied half.

Fix it by granting the account named in the message, which for most jobs is the Agent or engine service account rather than a person, and by writing to a UNC path instead of a drive letter wherever a job touches a remote target.


STEP6

Step 6: The Message Stops Before the Useful Part

Job history is not a log file, and a step that prints a lot loses the end of it. Measured on SQL Server 2025 (RTM-CU8, 17.0.4075.5): a step that printed 6,000 characters and then failed stored a history message of 2,122 characters in total, of which 1,999 were the step’s own output before it was cut. The column is nvarchar(4000), so the cap that bit was not the column. Turning on log-to-table for the same step wrote 2,221 characters into msdb.dbo.sysjobstepslogs, still nowhere near 6,000.

So if the message ends mid-sentence, assume the cause is in the part you cannot see, and do not spend time re-reading what survived. There are 2 ways forward. Set an output file on the step, @output_file_name on sp_add_jobstep in MS Docs, pointed at a folder the Agent service account can write to, and run it again. Or cut the noise at source: a step that prints progress for every table is the reason the error got pushed off the end, and quieting it is usually the better fix.

The same cap is why a job that fails intermittently is worth instrumenting before the next failure rather than after it, since the 1 run you care about is the one whose message got trimmed.


Maintenance Jobs Fail Differently, and More Quietly

A failed application job gets noticed because something stops arriving. A failed maintenance job gets noticed months later, during a restore. Nothing about the instance looks wrong in the meantime: the database is online, queries run, and index maintenance has simply not happened since March.

Get Maintenance Job Status is the one to run here rather than the general failure summary, because it reads the DBA % jobs specifically and returns last outcome, duration, the error message and the next scheduled run together. The last of those is the column people skip, and it catches a failure mode that never appears in job history at all: an enabled job with an empty next run time is not failing, it is not scheduled, usually because a schedule was deleted or disabled while the job itself stayed on. No run means no failure row, which means no alert, and nothing to find unless you go looking.

If the job in front of you came from the standard framework, the SQL Server Maintenance Job Framework is what it was generated from and what it should look like when it is healthy.


The Scripts That Replace the Click Through

Reading 1 job’s history in SSMS is 6 clicks and tells you about 1 job. These are the instance-wide versions, and they all live under DBA Scripts: SQL Agent and Jobs.

Two more are worth knowing about when the failure is not really the job’s fault. THREADPOOL Wait Type in SQL Server is the case where work sits queued waiting for a worker thread instead of failing, and Get Recent Error Log Entries is the instance-level view for the window the job failed in, since a step failure and an engine problem at the same minute are usually the same event.


Make It Loud Next Time

The job that failed tonight almost certainly failed before, and the reason nobody knew is that nothing was wired up to tell anyone. That is a 20 minute fix and it is worth doing while the incident is still fresh.

Get Agent Alerts and Operators checks the whole notification chain in 1 run: which of severities 17 to 25 have an enabled alert, whether every alert has a live operator attached, and whether Database Mail underneath them can send at all. An alert with no operator is the one that catches people out, because it looks configured in every view except the one that matters.

Test the mail path itself before you trust it, with EXEC msdb.dbo.sp_send_dbmail @profile_name = N'...', @recipients = N'...', @subject = N'Agent mail test'; and then confirm it left, in msdb.dbo.sysmail_faileditems as well as msdb.dbo.sysmail_sentitems. The procedure returns success when the message is queued, not when it is delivered, which is its own quiet failure. The parameters are in MS Docs for sp_send_dbmail, and Get Database Mail and xp_cmdshell Configuration tells you whether Database Mail is even enabled on the instance before you start.

Then set a notification on the job itself, so a failure emails an operator and the next one has somewhere to be seen that is not a person remembering to look.


Frequently Asked Questions

All it says is “The job failed. The Job was invoked by”. Where is the real error?
On a different row of the same table. That line is the job outcome row, step_id = 0, and its only job is to record that the run ended and which step it ended on. The cause is on the step row for the step it names, where step_id is greater than 0. The query in Step 1 filters the outcome rows out, so you only see the ones with a message worth reading.
The job history message is truncated. How do I see the rest of it?
You cannot recover it after the fact, so set the next run up to keep it. Measured on SQL Server 2025 (Step 6), a step that printed 6,000 characters stored 1,999 of them in the history message, and logging the step to a table only reached 2,221. Set @output_file_name on the step, pointed at a folder the Agent service account can write to, and run it again. If the step is noisy because it prints progress for every object, quieting it is usually a better fix than capturing more of it.
The job runs manually but fails on schedule.
Separate 2 different meanings of manually. If you ran the step’s T-SQL yourself in SSMS, it ran as you, and you are probably a sysadmin, so that tells you nothing about the job. The Executed as user: prefix on the step row says what the job actually ran as, which is Step 4. If you started the job itself and it succeeded, the context was the same both times, so look at what is different at that hour instead: another job overlapping the same window, a file that only exists after an upstream process has run, or a drive mapping that exists in your session and not in the service’s.
The job shows success but it did nothing.
Check the step flow before you check the code. A 2 step job whose first step is set to quit reporting success never reaches step 2, reports success, and says so in plain words on the outcome row: “The job succeeded. The last step to run was step 1”. Nothing in job history flags it, because as far as Agent is concerned nothing went wrong. Read msdb.dbo.sysjobsteps for on_success_action and on_fail_action across every step, and check each step’s database context while you are there, since a step pointed at the wrong database will also run clean and change nothing.
Why does it say it cannot determine if the owner of the job has server access?
The job’s owner is stored as a SID, and that SID no longer resolves to a login the server can ask about, so Agent refuses to run anything on its behalf. It is usually a Windows or domain account that was deleted or renamed, since SQL Server will not let you drop a login that owns a job. The query in Step 2 lists every job whose owner has gone, and sp_update_job with @owner_login_name reassigns them.

Related

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *