Dagster already knows when an asset finished building. DataOps TestGen knows whether the data in it is any good. The join between them is an asset check that calls the TestGen REST API.
TestGen runs from Dagster. There’s no connector, and you don’t need one. This page covers Dagster specifically; the Airflow answer and the CI/CD answer cover the same API calls in those tools.
The three calls
Every call carries Authorization: Bearer <token>, per API Authentication. The endpoints come from Run Profiling and Tests:
| Step | Call | What comes back |
|---|---|---|
| Start a test run | POST /api/v1/test-suites/{test_suite_id}/test-runs | 202 Accepted with the job id |
| Poll | GET /api/v1/jobs/{job_id} | status; stop on completed, error, or canceled |
| Read the result | GET /api/v1/test-runs/{job_id} | result.score and result.result_counts (passed, failed, warning, error) |
Submission returns at once. It doesn’t wait for the tests, so your code polls. Give the poll a timeout.
The asset check
import os, time, requests
import dagster as dg
API = os.environ["TESTGEN_API_URL"] # http://<host>:8530 by default
HDR = {"Authorization": f"Bearer {os.environ['TESTGEN_TOKEN']}"} # one-hour OAuth access token (or an Enterprise PAT)
def run_testgen_suite(suite_id, timeout_s=1800):
job_id = requests.post(f"{API}/api/v1/test-suites/{suite_id}/test-runs",
headers=HDR, timeout=30).json()["id"]
deadline = time.time() + timeout_s
while time.time() < deadline:
job = requests.get(f"{API}/api/v1/jobs/{job_id}", headers=HDR, timeout=30).json()
if job["status"] in ("completed", "error", "canceled"):
break
time.sleep(15)
else:
raise TimeoutError(f"TestGen job {job_id} did not finish")
if job["status"] != "completed":
raise RuntimeError(f"TestGen job {job_id}: {job['status']} {job['error_message']}")
return requests.get(f"{API}/api/v1/test-runs/{job_id}", headers=HDR, timeout=30).json()["result"]
@dg.asset_check(asset="orders_silver", blocking=True)
def orders_silver_testgen():
result = run_testgen_suite(os.environ["TG_SUITE_ORDERS_SILVER"])
counts = result["result_counts"]
blocked = counts["failed"] + counts["error"] > 0
return dg.AssetCheckResult(
passed=not (blocked or counts["warning"]),
severity=dg.AssetCheckSeverity.ERROR if blocked else dg.AssetCheckSeverity.WARN,
metadata={"score": result["score"], **counts},
)
The blocking=True part is Dagster’s: a blocking check that fails with ERROR severity stops downstream assets (docs checked October 2026). One gotcha from the same page: if the check emits no result, downstream assets run anyway and Dagster logs a warning. Raising on a TestGen error, as above, keeps a broken call from passing quietly.
What blocks and what doesn’t
TestGen gives each test a severity. A test set to warn returns warning instead of failed, per the testing docs. So the decision stays in TestGen, where the data steward can change it without a deploy. Failed and error results block. Warnings show up in Dagster as a WARN check and the run keeps going.
DataKitchen calls a blocking check like this a tripwire. The April 2026 post built one in Databricks over SSH because the API hadn’t shipped yet. It has now, so the tripwire is three HTTP calls.
Map suites to asset groups
Dagster organizes assets with group_name. Give each group its own TestGen test suite and one blocking check on the group’s last asset. Bronze, silver, and gold groups become three suites, which is the shape DataKitchen’s medallion testing post argues for: test between layers so gold never sees bad silver.
Where results land
In TestGen. The Test Results page holds every result, and the run accepts an optional body with external_url and metadata, which show up under Link and Run details. Put the Dagster run URL there and you can click from a failed test back to the run. The check’s metadata keeps the score and counts in Dagster.
Profiling runs the same way through POST /api/v1/table-groups/{table_group_id}/profiling-runs, but it doesn’t belong in every materialization. TestGen’s scheduling docs recommend profiling every two weeks to a month, so let TestGen’s scheduler run it.
Tokens and Observability
Open source authenticates with an OAuth 2.1 client: trade a stored refresh token for a one-hour access token at the start of each run. Refresh tokens last 30 days, and a new one takes a browser sign-in, so put the renewal on a calendar. Enterprise adds personal access tokens.
DataKitchen documents no DataOps Observability agent for Dagster. TestGen can still export its results to Observability.
Documentation: TestGen API, Run Profiling and Tests, API Authentication, OAuth 2.1 Clients, Connect to Observability