Home / Databases / SQL Server

DatabasesHow-toAdvanced

Run job B only after job A succeeded today: table-driven SQL Agent job dependencies

Agent has no built-in dependency between jobs, so people chain them with sp_start_job or guess at schedule gaps. Instead, keep dependencies in a table and add a gate step that checks predecessors succeeded for today's batch. Each job can then wait, skip or fail, and the whole chain is visible in one query.

Deepak Bhardwaj3 min read

Applies toSQL Server 2016–2025, Azure SQL Managed Instance

The usual approaches and their problems

Approach Problem
Schedule B 30 minutes after A A runs long, B starts on stale data
A's last step starts B Hidden chain, hard to rerun B alone, A succeeds even if B fails
One giant job Can't run or rerun parts independently

The table-driven design

CREATE TABLE dbo.JobDependency (
  job_name         sysname NOT NULL,
  depends_on_job   sysname NOT NULL,
  CONSTRAINT PK_JobDependency PRIMARY KEY (job_name, depends_on_job));

INSERT dbo.JobDependency VALUES
 (N'ETL - Transform Sales', N'ETL - Extract Sales'),
 (N'ETL - Transform Sales', N'ETL - Extract Products'),
 (N'ETL - Publish Mart',    N'ETL - Transform Sales');

Each dependent job gets a schedule and a first step that checks its predecessors.

The gate procedure

CREATE OR ALTER PROCEDURE dbo.usp_JobGate
  @JobName sysname,
  @WaitMinutes int = 120,       -- how long to wait for predecessors
  @PollSeconds int = 60
AS
SET NOCOUNT ON;
DECLARE @today int = CONVERT(int, CONVERT(char(8), GETDATE(), 112)),
        @deadline datetime = DATEADD(MINUTE, @WaitMinutes, GETDATE()),
        @delay char(8) = CONVERT(char(8), DATEADD(SECOND, @PollSeconds, 0), 108),
        @missing nvarchar(max);

WHILE 1 = 1
BEGIN
  SELECT @missing = STRING_AGG(d.depends_on_job, ', ')
  FROM dbo.JobDependency d
  WHERE d.job_name = @JobName
    AND NOT EXISTS (
      SELECT 1 FROM msdb.dbo.sysjobhistory h
      JOIN msdb.dbo.sysjobs j ON j.job_id = h.job_id
      WHERE j.name = d.depends_on_job AND h.step_id = 0
        AND h.run_status = 1 AND h.run_date = @today);

  IF @missing IS NULL RETURN;    -- all predecessors succeeded today

  IF GETDATE() > @deadline
  BEGIN
    DECLARE @msg nvarchar(2000) = CONCAT(N'Predecessors not complete: ', @missing);
    THROW 50010, @msg, 1;
  END
  WAITFOR DELAY @delay;
END

Step 1 of ETL - Transform Sales:

EXEC DBA.dbo.usp_JobGate @JobName = N'ETL - Transform Sales', @WaitMinutes = 180;

On failure: quit reporting failure. The notification tells you which predecessor didn't finish.

Define "today"

"Succeeded today" by run_date is simple but breaks for batches that cross midnight. If your batch starts at 22:00, use a business date instead: have each job write a row to dbo.BatchRun (job_name, business_date, status) in its last step and check that table, not msdb history.

See the whole chain

WITH last AS (
  SELECT j.name, h.run_status, h.run_date, h.run_time,
         ROW_NUMBER() OVER (PARTITION BY j.name ORDER BY h.instance_id DESC) rn
  FROM msdb.dbo.sysjobhistory h JOIN msdb.dbo.sysjobs j ON j.job_id = h.job_id
  WHERE h.step_id = 0)
SELECT d.job_name, d.depends_on_job, l.run_status AS predecessor_status, l.run_date, l.run_time
FROM dbo.JobDependency d
LEFT JOIN last l ON l.name = d.depends_on_job AND l.rn = 1
ORDER BY d.job_name;

Trade-offs

  • A waiting gate step occupies an Agent worker thread and shows the job as "running". That's fine for a handful of jobs. Prefer short waits plus a later retry schedule if you have many.
  • Rerunning a predecessor by hand counts as success for the day. Usually that's what you want.
  • Cross-server dependencies: query the other server's history through a linked server, or have predecessors write to a shared BatchRun table.

Tip

When the chain grows beyond 10–15 jobs with branches, consider a real orchestrator (ADF, Airflow, SSIS master package). The table approach is for keeping small estates simple.

Checklist

  • Dependencies in a table, reviewed with job owners
  • Gate step first in each dependent job
  • Business date for cross-midnight batches
  • Chain status query in your runbook