SQL Agent system stored procedures

Status: Reviewed
Stack: PowerShell → SQL / stored procs → Blazor
Depends on: SQL-CLI-09 (overview), SQL-CLI-01 (calling), SQL-CLI-07 (pipeline shapes)
Goal: Know when to use msdb Agent control-plane procs (sp_help_job*, sp_start_job / sp_stop_job, sp_update_job, …) vs querying msdb tables for Blazor inventory — and the permission / multi-set / platform gotchas.

Sources: Microsoft Learn (sql-server-ver17 / 2026). Expands SQL-CLI-09 §3.4 only — do not treat this as a rewrite of the overview. Calling: SQL-CLI-01. Staging → Blazor: SQL-CLI-07.


1. Scope map

In this doc (09d) Out → sibling
Agent control plane: sp_help_job*, sp_start_job / sp_stop_job, sp_update_job, thin sp_add_job* Config / sp_configure / who → 09a
History: sp_help_jobhistory, sp_purge_jobhistory Object help / space / rename → 09b
Schedules / steps help: sp_help_schedule, sp_help_jobstep Security / principals / orphans → 09c
Fixed Agent roles (SQLAgent*Role) Surface area / xp_*09e
Dashboard shapes: sysjobs, sysjobsteps, sysjobhistory, sysjobactivity, sysschedules, sysjobschedules Full call patterns / decision table → CLI-09

Control plane (ops): Agent procs in msdb — start/stop, enable/disable, interactive help.
Read path for Blazor: always msdb tables (single-select joins) — never scrape multi-result sp_help_job into a grid.

Platform: SQL Server Agent lives on SQL Server and Azure SQL Managed Instance (MI has feature gaps). Azure SQL Database has no Agent — use Elastic Jobs (jobs.sp_* in the job DB) or external schedulers. Same proc names in Elastic Jobs are a different API.


2. Decision: Agent procs vs msdb tables / DMVs

Need Prefer Why
One-off peek in SSMS / sqlcmd sp_help_job / sp_help_jobactivity / sp_help_jobhistory Fast human dump; filters built in
Inventory → staging → Blazor msdb.dbo.sysjobs* tables One result set, typed columns, WHERE/JOIN, stable shape
Start / stop a job now sp_start_job / sp_stop_job Supported control plane — don’t reinvent with service calls
Enable / disable job (ops) sp_update_job @enabled Scriptable; OperatorRole can flip enable on jobs they don’t own (name/id + @enabled only)
Create new jobs at scale SMO / scripted job defs / SSDT (thin use of sp_add_job*) Prefer checked-in scripts over ad-hoc sp_add_* chains
Runtime “is it running?” for dashboards sysjobactivity (current Agent session) Live dates; better than parsing help-proc text
Run history grids sysjobhistory + DATETIMEFROMPARTS Documented int→datetime pattern; filterable
Next-run for UI Prefer sysjobactivity.next_scheduled_run_date sysjobschedules can lag (~20 min refresh per Learn)
Purge old history sp_purge_jobhistory (careful) Control plane; OperatorRole / sysadmin for broad purge

Multi-result-set problem (same rule as 09b / 09c)

Caller Behavior
sqlcmd / go-sqlcmd Prints all sets — fine for eyeballs; ugly for -o CSV / parsers
Invoke-Sqlcmd Typically surfaces the last result set — earlier sets silently dropped
ADO.NET / SqlCommand Can walk NextResult() — not worth it for inventory

Rule for this stack: Collect with a single-select against msdb tables into dbo.*Staging (CLI-07). Use Agent help procs for interactive triage; use start/stop/update for control plane only.


3. Curated high-value procs

All live in msdb. Call as EXEC msdb.dbo.sp_… when the session DB isn’t msdb. Columns per proc: purpose · args · quirks · perms · prefer-instead.

3.1 Inventory / help

sp_help_job

Purpose Job metadata; with @job_name/@job_id, also steps + schedules + target servers
Args Optional filters: @job_name / @job_id, @job_aspect (ALL/JOB/SCHEDULES/STEPS/TARGETS), @job_type (LOCAL/MULTI-SERVER), @enabled, @execution_status, owner/category/subsystem/dates, …
Result quirks No job id/name → one job-summary set. With id/name → multiple sets (job + steps + schedules + targets). last_run_* / next_run_* are int (yyyyMMdd / HHmmss)
Perms SQLAgentUserRole (owned only) / Reader / Operator / sysadmin
Prefer instead sysjobs (+ sysjobsteps / sysjobschedules / sysschedules) for Blazor
EXEC msdb.dbo.sp_help_job;                                      -- interactive list
EXEC msdb.dbo.sp_help_job @job_name = N'NightlyBackups', @job_aspect = N'ALL';

sp_help_jobactivity

Purpose Snapshot of runtime state for the Agent session
Args Optional @job_id or @job_name; optional @session_id (default = most recent session)
Result quirks One set: run_requested_date, start_execution_date, stop_execution_date, next_scheduled_run_date, run_status, … Session IDs live in msdb.dbo.syssessions (new session each Agent service start)
Perms Agent roles; only sysadmin sees activity for jobs owned by others
Prefer instead sysjobactivity joined to sysjobs filtered to MAX(session_id)
EXEC msdb.dbo.sp_help_jobactivity;  -- all jobs caller can see (latest session)

sp_help_jobhistory

Purpose Filtered job / step history report
Args @job_name / @job_id, @step_id, @run_status, date/time ints (yyyyMMdd / HHmmss), @mode (SUMMARY default / FULL), @oldest_first, @server, …
Result quirks Column list depends on @mode. Dates/times/durations are ints, not datetime
Perms UserRole = owned jobs only; Reader/Operator/sysadmin broader
Prefer instead sysjobhistory for dashboards
EXEC msdb.dbo.sp_help_jobhistory @job_name = N'NightlyBackups', @mode = N'FULL';

sp_help_jobstep / sp_help_schedule

Proc Purpose Key args Prefer instead
sp_help_jobstep Steps for one job @job_name or @job_id; optional @step_id / @step_name; @suffix sysjobsteps
sp_help_schedule Schedule definitions @schedule_id / @schedule_name; @attached_schedules_only; @include_description sysschedules (+ sysjobschedules for job link)

UserRole: owned jobs/schedules only.

3.2 Control plane

sp_start_job / sp_stop_job

sp_start_job sp_stop_job
Purpose Run job immediately Signal Agent to stop a running job
Args @job_name or @job_id; optional @step_name (local), @server_name (target) @job_name / @job_id / @originating_server (mutually exclusive among first three); optional @server_name for MSX
Result None (return code 0/1) None
Perms User/Reader: owned only. Operator: all local. sysadmin: local + multiserver Same split
Gotchas Applies to SQL Server + MI. Name collision with Elastic Jobs jobs.sp_start_job Stop is a signal — long BACKUP/RESTORE/DBCC may finish a stable point first. CmdExec/PowerShell steps can be force-killed (files left open) — extreme cases only
EXEC msdb.dbo.sp_start_job @job_name = N'Weekly Sales Data Backup';
EXEC msdb.dbo.sp_stop_job  @job_name = N'Weekly Sales Data Backup';

sp_update_job (ops focus)

Purpose Change job attributes; most common scripted use = enable/disable
Args @job_name or @job_id; then only what you change: @enabled, @new_name, @description, notify_*, @owner_login_name, …
Quirks Omitted params keep current values. Owner change: T-SQL steps run as the job owner — changing owner changes security context. Only sysadmin can change ownership
OperatorRole note Can enable/disable jobs they don’t own via @job_name/@job_id + @enabled only — any other param → fail
Prefer Keep for control plane; don’t build create/replace pipelines here
EXEC msdb.dbo.sp_update_job @job_name = N'NightlyBackups', @enabled = 0;

sp_add_job* family (thin)

Stance Detail
Exists sp_add_job, sp_add_jobstep, sp_add_schedule, sp_attach_schedule, sp_add_jobserver, …
Rick’s habit Prefer checked-in job scripts / SMO / existing defs for create & promote. Ad-hoc sp_add_* chains drift and skip review
Ops day-to-day Inventory + start/stop + enable/disable — not greenfield job authoring in this doc

3.3 History purge

sp_purge_jobhistory

Purpose Delete history rows (optionally older than @oldest_date)
Args @job_name or @job_id; optional @oldest_date. sysadmin / SQLAgentOperatorRole may omit job → purge all (sysadmin: local+multiserver; Operator: local only)
Perms Default: sysadmin or SQLAgentOperatorRole. User/Reader need explicit EXECUTE and then only on owned jobs
Risk Easy to wipe fleet history — always pass @oldest_date (and usually a job name) in scripts
EXEC msdb.dbo.sp_purge_jobhistory
  @job_name = N'NightlyBackups',
  @oldest_date = '2025-01-01';

4. Permissions (SQL Agent fixed database roles)

Roles live in msdb, concentric (more privilege ⊆ less). Users need one of these or sysadmin to use Agent; otherwise the Agent node is invisible in SSMS.

Role Inventory / history Start/stop Enable/disable others’ local jobs Purge history Multiserver
SQLAgentUserRole Owned jobs/schedules Owned only No (owned only) No by default (needs explicit EXEC on purge; owned only) No
SQLAgentReaderRole All local + multiserver view Owned only No Same as UserRole View yes; control no
SQLAgentOperatorRole All view All local Yes via sp_update_job @enabled only All local View yes; start/stop/purge multiserver no
sysadmin Full Full (local + multiserver) Full Full Full

Proxy inheritance: higher roles inherit proxies granted to lower roles — grant proxies carefully.

Collector login for Blazor: prefer SQLAgentReaderRole (read-wide) without Operator purge/start power, unless the app must trigger jobs (then Operator or a dedicated control path).


5. Gotchas

Gotcha Detail
Multi-result sp_help_job Passing @job_name returns steps/schedules/targets as extra setsInvoke-Sqlcmd last-set trap
Int dates/times History and many help columns use yyyyMMdd / HHmmss ints — convert for UI (see §6)
History retention On-prem: Agent properties (max rows / max per job). MI: Agent properties are read-only; history caps fixed at defaults (1000 total, 100 per job) — plan external archival if you need more
sysjobschedules lag Learn: table may refresh on a ~20 minute cycle — don’t trust next_run_* there for live UI; use sysjobactivity
Local vs multiserver (MSX/TSX) OperatorRole does not start/stop/purge multiserver jobs — need sysadmin. MSX not supported on MI
Azure SQL DB No SQL Agent / no msdb Agent tables. Elastic Jobs = different jobs.sp_* API
MI gaps Agent always on; no proxies; limited notifications; no CPU-idle schedules; PowerShell/CmdExec limits — see Learn SQL Agent job limitations in SQL Managed Instance
sp_stop_job on CmdExec/PS Can force-kill the process — prefer graceful step design
Job owner context T-SQL steps run as owner — ownership changes are security changes
Name collisions sp_start_job / sp_stop_job / sp_purge_jobhistory also exist under Elastic Jobs schema — wrong database = wrong product

6. Blazor inventory via msdb tables (not help procs)

Single-select shapes → stage → Blazor (CLI-07). Prefer documented DATETIMEFROMPARTS over undocumented helpers.

6.1 Job list

SET NOCOUNT ON;
SELECT
  j.job_id,
  j.name AS job_name,
  j.enabled,
  j.category_id,
  SUSER_SNAME(j.owner_sid) AS owner_name,
  j.date_created,
  j.date_modified,
  j.description,
  SYSUTCDATETIME() AS collected_utc
FROM msdb.dbo.sysjobs AS j
ORDER BY j.name;

6.2 Current activity (latest Agent session)

SET NOCOUNT ON;
SELECT
  j.name AS job_name,
  a.start_execution_date,
  a.stop_execution_date,
  a.next_scheduled_run_date,
  a.last_executed_step_id,
  CASE
    WHEN a.start_execution_date IS NOT NULL AND a.stop_execution_date IS NULL THEN N'Running'
    ELSE N'Not running'
  END AS run_status_label
FROM msdb.dbo.sysjobs AS j
INNER JOIN msdb.dbo.sysjobactivity AS a ON a.job_id = j.job_id
WHERE a.session_id = (SELECT MAX(session_id) FROM msdb.dbo.sysjobactivity);

6.3 Recent history (int → datetime)

SET NOCOUNT ON;
SELECT TOP (500)
  j.name AS job_name,
  h.step_id,
  h.step_name,
  h.run_status,
  DATETIMEFROMPARTS(
    h.run_date / 10000, h.run_date % 10000 / 100, h.run_date % 100,
    h.run_time / 10000, h.run_time % 10000 / 100, h.run_time % 100, 0
  ) AS run_start,
  (h.run_duration / 10000) * 3600
    + ((h.run_duration % 10000) / 100) * 60
    + (h.run_duration % 100) AS run_duration_seconds,
  h.message
FROM msdb.dbo.sysjobhistory AS h
INNER JOIN msdb.dbo.sysjobs AS j ON j.job_id = h.job_id
ORDER BY h.instance_id DESC;

6.4 Steps + schedules (definitions)

-- Steps
SELECT j.name AS job_name, s.step_id, s.step_name, s.subsystem,
       s.database_name, s.command
FROM msdb.dbo.sysjobsteps AS s
JOIN msdb.dbo.sysjobs AS j ON j.job_id = s.job_id;

-- Schedules attached to jobs
SELECT j.name AS job_name, sch.name AS schedule_name, sch.enabled,
       sch.freq_type, js.next_run_date, js.next_run_time
FROM msdb.dbo.sysjobs AS j
JOIN msdb.dbo.sysjobschedules AS js ON js.job_id = j.job_id
JOIN msdb.dbo.sysschedules AS sch ON sch.schedule_id = js.schedule_id;

7. Copy-paste recipes (sqlcmd + Invoke-Sqlcmd)

Assume Windows auth (-E). Swap to CLI-06 for Entra/SQL auth. Always -b / check exit codes (CLI-01, CLI-07).

R1 — List jobs (interactive help)

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q "EXEC dbo.sp_help_job;"

R2 — Job activity snapshot

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q "EXEC dbo.sp_help_jobactivity;"

R3 — Start a job

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q \
  "EXEC dbo.sp_start_job @job_name = N'NightlyBackups';"

R4 — Stop a job

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q \
  "EXEC dbo.sp_stop_job @job_name = N'NightlyBackups';"

R5 — Disable / enable via update

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q \
  "EXEC dbo.sp_update_job @job_name = N'NightlyBackups', @enabled = 0;"

R6 — Full history for one job (interactive)

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q \
  "EXEC dbo.sp_help_jobhistory @job_name = N'NightlyBackups', @mode = N'FULL';"

R7 — Job inventory CSV (tables → Blazor staging)

$q = @"
SET NOCOUNT ON;
SELECT j.name AS JobName, j.enabled AS IsEnabled,
       SUSER_SNAME(j.owner_sid) AS OwnerName,
       j.date_modified AS DateModified, SYSUTCDATETIME() AS CollectedUtc
FROM msdb.dbo.sysjobs AS j
ORDER BY j.name;
"@
Invoke-Sqlcmd -ServerInstance $env:SQL_SERVER -Database msdb -TrustServerCertificate -Query $q |
  Export-Csv ".\staging\jobs_$($env:SQL_SERVER -replace '\\','_').csv" -NoTypeInformation

R8 — Running jobs (activity table)

$q = @"
SET NOCOUNT ON;
SELECT j.name AS JobName, a.start_execution_date AS StartExecutionDate,
       a.last_executed_step_id AS LastStepId
FROM msdb.dbo.sysjobs AS j
JOIN msdb.dbo.sysjobactivity AS a ON a.job_id = j.job_id
WHERE a.session_id = (SELECT MAX(session_id) FROM msdb.dbo.sysjobactivity)
  AND a.start_execution_date IS NOT NULL
  AND a.stop_execution_date IS NULL;
"@
Invoke-Sqlcmd -ServerInstance $env:SQL_SERVER -Database msdb -TrustServerCertificate -Query $q |
  ConvertTo-Json -Depth 3 |
  Set-Content ".\running-jobs-$($env:SQL_SERVER -replace '\\','_').json" -Encoding UTF8

R9 — Failed history last 7 days → JSON

$q = @"
SET NOCOUNT ON;
SELECT j.name AS JobName, h.step_name AS StepName, h.run_status AS RunStatus,
       DATETIMEFROMPARTS(
         h.run_date/10000, h.run_date%10000/100, h.run_date%100,
         h.run_time/10000, h.run_time%10000/100, h.run_time%100, 0) AS RunStart,
       h.message AS 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
  AND DATETIMEFROMPARTS(
        h.run_date/10000, h.run_date%10000/100, h.run_date%100,
        h.run_time/10000, h.run_time%10000/100, h.run_time%100, 0)
      >= DATEADD(DAY, -7, SYSUTCDATETIME());
"@
Invoke-Sqlcmd -ServerInstance $env:SQL_SERVER -Database msdb -TrustServerCertificate -Query $q |
  ConvertTo-Json -Depth 3 |
  Set-Content ".\job-failures-7d.json" -Encoding UTF8

R10 — Purge history older than retention (named job)

sqlcmd -S "$SQL_SERVER" -E -d msdb -b -Q \
  "EXEC dbo.sp_purge_jobhistory @job_name = N'NightlyBackups', @oldest_date = '2025-01-01';"

8. Pointers to 09 / 09a–c / 09e

Doc Covers
SQL-CLI-09 Overview: call patterns, procs vs DMVs, curated catalog (§3.4 Agent)
SQL-CLI-09a sp_configure / RECONFIGURE; instance helpers
SQL-CLI-09b Object help, space, text, rename; multi-set trap
SQL-CLI-09c Logins / users / roles / orphans / permissions reporting
09d (this) Agent control plane vs msdb tables; roles; recipes
09e Surface area / xp_cmdshell / Ole Automation — policy & safer alternatives

Related stack: SQL-CLI-01 (calling / -b / -d), SQL-CLI-07 (Collect→Stage→Blazor).

Learn anchors (ver17): SQL Server Agent Tables, SQL Server Agent Fixed Database Roles, sp_help_job, sp_help_jobactivity, sp_help_jobhistory, sp_help_jobstep, sp_help_schedule, sp_start_job, sp_stop_job, sp_update_job, sp_purge_jobhistory, dbo.sysjobs, dbo.sysjobsteps, dbo.sysjobhistory, dbo.sysjobactivity, dbo.sysschedules, dbo.sysjobschedules, Job automation with SQL Agent Jobs (Azure SQL Managed Instance), Elastic Jobs overview (Azure SQL Database).