How do you add data quality checks after an SSIS package runs?

Add a task after the data flow that runs your checks and fails when they fail. The simplest is an Execute SQL Task whose T-SQL counts bad rows and raises an error with THROW, so a precedence constraint stops downstream work. For more than a handful of checks, an Execute Process Task can start a test run in a tool like DataOps TestGen and fail on its result.

Key points

  • An SSIS Execute SQL Task runs SQL statements or stored procedures from a package, and a T-SQL THROW in it fails the task, so a Success precedence constraint keeps downstream tasks from running.
  • With OLE DB and ADO connection managers, Microsoft says the Execute SQL Task ignores errors that come after the first result set, so raise the error before any SELECT returns rows and use SET NOCOUNT ON.
  • With FailTaskIfReturnCodeIsNotSuccessValue set, the SSIS Execute Process Task fails when the process exit code differs from its SuccessValue, 0 by default, so a script that exits 1 on failed tests stops the package.
  • SQL Server 2025 Integration Services deprecates the legacy Integration Services Service and 32-bit mode, and removes the Attunity CDC components, the Microsoft Connector for Oracle, and the Hadoop tasks, per Microsoft Learn checked November 2026.
  • DataOps TestGen starts a test run with POST /api/v1/test-suites/{test_suite_id}/test-runs and reports failed and error counts, and DataOps Observability has a documented SSIS agent that shows package runs beside test results.

SSIS tells you whether a package succeeded. It doesn’t tell you whether the 40,000 rows it loaded are right. The fix is a check step between the load and whatever reads it, wired so a failed check stops the package.

Option 1: an Execute SQL Task

Put an Execute SQL Task after the Data Flow Task, on the same OLE DB connection, with ResultSet set to None:

SET NOCOUNT ON;

DECLARE @null_keys int, @dupe_keys int, @orphans int, @msg nvarchar(2048);

SELECT @null_keys = COUNT(*)
FROM dbo.FactSales
WHERE CustomerKey IS NULL;

SELECT @dupe_keys = COUNT(*)
FROM (
    SELECT SalesOrderNumber, SalesOrderLineNumber
    FROM dbo.FactSales
    GROUP BY SalesOrderNumber, SalesOrderLineNumber
    HAVING COUNT(*) > 1
) AS d;

SELECT @orphans = COUNT(*)
FROM dbo.FactSales AS f
WHERE f.CustomerKey IS NOT NULL
  AND NOT EXISTS (SELECT 1 FROM dbo.DimCustomer AS c WHERE c.CustomerKey = f.CustomerKey);

IF @null_keys + @dupe_keys + @orphans > 0
BEGIN
    SET @msg = CONCAT(N'Data quality check failed: ', @null_keys, N' null keys, ',
                      @dupe_keys, N' duplicate order lines, ', @orphans, N' orphaned customer keys.');
    THROW 50001, @msg, 1;
END;

The THROW fails the task. Connect the next task with a Success precedence constraint and a Failure constraint to a Send Mail Task or a logging step.

Two details from Microsoft’s docs. With OLE DB and ADO connection managers, the task ignores errors returned after the first result set, so this script assigns variables and returns no rows before it throws. And SET NOCOUNT ON keeps row-count messages out of the way.

This works well for five checks on two tables. It doesn’t scale to fifty tables, and it keeps no history, so you can’t see that duplicates have been creeping up all month.

Option 2: call a test tool with an Execute Process Task

The Execute Process Task runs a program and, with FailTaskIfReturnCodeIsNotSuccessValue on, fails when the exit code isn’t the SuccessValue, 0 by default. Point it at powershell.exe with a script that starts a DataOps TestGen test run through its REST API, waits, and exits 1 on failures:

param([string]$Api, [string]$SuiteId, [string]$Token)
$h = @{ Authorization = "Bearer $Token" }
$job = Invoke-RestMethod -Method Post -Uri "$Api/api/v1/test-suites/$SuiteId/test-runs" `
       -Headers $h -ContentType 'application/json' -Body '{"metadata": {"system": "SSIS"}}'
$deadline = (Get-Date).AddMinutes(30)
do {
    Start-Sleep -Seconds 15
    $status = (Invoke-RestMethod -Uri "$Api/api/v1/jobs/$($job.id)" -Headers $h).status
} while ($status -notin @('completed', 'error', 'canceled') -and (Get-Date) -lt $deadline)
if ($status -ne 'completed') { exit 1 }
$counts = (Invoke-RestMethod -Uri "$Api/api/v1/test-runs/$($job.id)" -Headers $h).result.result_counts
if (($counts.failed + $counts.error) -gt 0) { exit 1 }
exit 0

TestGen returns 202 Accepted with a job id at once, so the script polls until the job reaches completed, error, or canceled. Warnings don’t fail it on purpose. Keep the token out of the package: pass it from an SSIS sensitive parameter or read it from a vault. TestGen’s API authentication page covers tokens; on open source they come from an OAuth client.

The tests themselves aren’t hand-written. TestGen profiles the tables the package loads, 55 characteristics per column, and generates tests from 34 test types, including duplicate rows. You add Reference Match for the orphaned-key check above.

What SQL Server 2025 changes

SSIS ships with SQL Server 2025, with changes (What’s New in Integration Services 2025, checked November 3, 2026). The legacy Integration Services Service and 32-bit mode are deprecated. The Attunity CDC components, the Microsoft Connector for Oracle, and the Hadoop tasks are gone. Neither check pattern above depends on them.

Seeing the package and the tests together

DataOps Observability has a documented SSIS agent, and TestGen exports results to it, so the package run and the tests on the table it loaded sit on one timeline. The Data Factory answer shows the same pattern for ADF.

Open-source TestGen is free for one user, one connection, and one project. Start at the SQL Server install page.

Frequently asked questions

Short answers to the follow-up questions people ask next.

How do I make an SSIS package fail when a data quality check fails?

Raise an error in the check. In an Execute SQL Task, count the bad rows into variables and run THROW when the count is above zero; the task fails, and a Success precedence constraint stops the next task. With an OLE DB connection, raise the error before any result set, because the task ignores errors that come after the first one.

Should checks run inside the data flow or after it?

Both, for different jobs. Inside the data flow, a Conditional Split or a Lookup's no-match output routes individual bad rows away from the destination as they load. After the data flow, set-based checks see the whole table: duplicates across batches, orphaned keys, totals, and volume compared with yesterday. Those only make sense once the load has finished.

What happens to SSIS in SQL Server 2025?

It ships, with changes. Microsoft's SQL Server 2025 notes deprecate the legacy Integration Services Service and 32-bit mode, and remove the Attunity CDC components, the Microsoft Connector for Oracle, and the Hadoop tasks. The ADO.NET connection manager adds the Microsoft SqlClient Data Provider. Projects that use the SSIS .NET runtime API need updated references if their packages include an Execute SQL Task.

Can TestGen see my SSIS package runs?

TestGen tests the tables the package loads. DataOps Observability, DataKitchen's other open-source tool, has a documented SSIS integration agent that reports package runs, and TestGen can export its results to Observability, so a failed test appears next to the package run that loaded the table.

Try it on your own data

DataOps TestGen is free and open source. Profile a table and generate its tests this afternoon.