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 says | What 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 n | Names 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 access | The 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 5 | 3 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 sentence | History 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- Get SQL Agent Job Failure Summary, every failure in the last 7 days with the real step message. This is the one to run first, during the incident.
- SQL Server Agent Will Not Start, or “Agent XPs Component Is Turned Off” (Error 15281), the service-vs-job distinction, where the service starts but the jobs fail
- Get SQL Agent Job Overview, every job with whether it is enabled, who owns it and how its last run went. Run it second, to find out whether this failure is 1 job or a pattern.
- Get Job Schedules and Duration Trends, when each job runs next, and which jobs are quietly taking longer than their own 30-day average. A job that fails on a timeout was usually slowing down for weeks first.
- DBA Scripts: Enable or Disable All SQL Agent Jobs, the script that snapshots the enabled flag for every job before a maintenance window, so a bulk disable and the bulk re-enable never drift apart.
- Generate Agent Job Script, when the answer is to rebuild the job somewhere else rather than repair it in place.
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?
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?
@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.
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.
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?
sp_update_job with @owner_login_name reassigns them.Related
- DBA Scripts: SQL Agent and Jobs, every Agent and job script on this site in one place
- DBA Scripts: Maintenance and Automation, the pillar this page sits under
- DBA Scripts: Get SQL Agent Job Failure Summary, the first script to run when a job has failed
- SQL Server Errors: The Complete Guide, every error number on this site, searchable
Leave a Reply