Suite document¶
A suite written as a file: its name, description and checks, with no database identity and no connection. Export produces one, import creates a suite from one, and validate tells you whether an import would be accepted. Keep it in a repository beside the pipeline it guards.
The document can be JSON or YAML. Both mean exactly the same thing.
version: 1
name: Orders quality
description: Checks on the orders table after the nightly load
checks:
- name: order id is never null
expectation_type: expect_column_values_to_not_be_null
config:
column: order_id
- name: status is a known value
expectation_type: expect_column_values_to_be_in_set
config:
column: status
value_set: [NEW, PAID, SHIPPED, CANCELLED]
mostly: 0.99
warn_threshold: 0.5
fail_threshold: 2
- name: orders arrived today
kind: freshness
expectation_type: monitor:freshness
config:
column: order_ts
warn_threshold: 24
fail_threshold: 48
Fields¶
Document¶
| Field | Required | Meaning |
|---|---|---|
version |
no (default 1) |
The document format. This page describes version 1; any other value is refused. |
name |
yes | The suite's name, 1–128 characters. |
description |
no | Up to 1024 characters. |
checks |
no | A list of checks. An empty suite is valid. |
An unknown field anywhere in the document is refused, so a typo in a field name is an error and never a silently ignored line.
Check¶
| Field | Required | Meaning |
|---|---|---|
name |
yes | 1–256 characters. |
expectation_type |
yes | The check type. Every type, its parameters and a worked example of each is on Check types. |
kind |
no (default expectation) |
expectation, freshness, volume, schema_drift, anomaly, aggregate, pii_drift or comparison. Must match the type: a monitor:freshness type has kind freshness. |
config |
no | The type's parameters, as shown for each type on Check types. |
warn_threshold, fail_threshold, critical_threshold |
no | Severity bands. They must be ordered warn ≤ fail ≤ critical, and only types that measure a number accept them. |
dimension |
no | accuracy, completeness, consistency, integrity, timeliness, uniqueness or validity. Leave the field out to have it derived from the check type. Write dimension: null to keep the check unclassified on purpose. |
engine |
no (default gx) |
What evaluates the check. A native engine (dmf, dqx) imports only onto a connection that offers it. |
enabled |
no | false keeps the check in the suite but leaves it out of every run. An export writes the field only for a check that is switched off. On import, a check without the field is created switched on. |
source_connection |
comparison checks only | {name, env} of the connection the comparison reads its source from. It must already exist in the workspace under that name and environment. |
What the document does not carry¶
- The connection. You name it when you import or validate, so the same file can be applied to DEV and to PROD.
- The target table or file. An imported suite has no target and cannot run until you set one, in the app or through the API.
- Schedules, triggers, sharing and notification settings. These belong to the suite in the workspace, not to the document.
- Run history. Importing always creates a new suite. To change an existing suite from a file, use apply.
YAML rules¶
DataQ reads YAML with JSON's rules for plain values, so a value is what it looks like:
| You write | DataQ reads |
|---|---|
true, false |
a boolean |
42, -2, 1.5, 1e3 |
a number |
null, ~, or nothing |
null |
| anything else, quoted or not | text |
So NO, yes, off, 2026-01-01, 1:30 and 010 are all text. Many YAML tools would
read those as a boolean, a date or a number; DataQ does not, and a country code or a
zero-padded identifier in a value_set stays what you wrote. To pass a number or true as
text, quote it: "42".
Not supported, and refused with the line number:
- anchors and aliases (
&name,*name) and merge keys (<<) - explicit tags (
!!binary,!!timestamp, …) - keys that are not text
- nesting more than 32 levels deep
- documents longer than 1,000,000 characters
A YAML export quotes every text value another YAML tool might read as something else
("no", "2026-01-01", "0x1F"), so the file survives being loaded and rewritten elsewhere.
Validate before you import¶
POST /api/v1/suites/validate runs every rule import applies and reports all the
problems, where import stops at the first. It creates nothing.
curl -sS https://dataq.example.com/api/v1/suites/validate \
-H "Authorization: Bearer $DATAQ_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg c "$CONNECTION_ID" --rawfile y orders.yaml \
'{connection_id: $c, document_yaml: $y}')"
{
"valid": false,
"check_count": 3,
"problems": [
{
"location": "checks[1]",
"check_name": "status is a known value",
"code": "check_config_invalid",
"message": "warn_threshold (2) must be <= fail_threshold (0.5) — severity thresholds band an increasingly bad metric, so they must be non-decreasing"
}
]
}
That is the example above with the second check's warn_threshold and fail_threshold
swapped. location is document for a problem with the document as a whole, checks[1] for the
second check, or a field path such as checks[1].expectation_type when a field is missing or
misspelled. Branch on code, not on the message.
valid: true means an import onto that connection would be accepted. It does not mean
the checks would pass, or even run: validation opens no datasource, so it cannot tell you
that a column named in a check does not exist. A file that cannot be read as YAML at all is
a 422 with the line and column.
Validation is against a connection because some rules depend on its type: a native-engine check, a type that only runs on a dataframe, a comparison's source.
Apply a file onto an existing suite¶
POST /api/v1/suites/{id}/apply brings a suite in line with a document. It is idempotent:
applying the same file twice changes nothing the second time.
Checks are matched by name:
| The check is | What apply does |
|---|---|
| in the file, not in the suite | creates it |
| in both, and differs | updates it |
| in both, and the same | nothing |
in both, and the file sets enabled to something else |
switches it. A file that does not mention enabled leaves the switch as it is, so re-applying an older file never starts a disabled check running again; write enabled: true to switch one on |
| in the suite, not in the file | leaves it alone, and lists it under unmanaged. With prune: true, deletes it together with its results and history. |
The suite's name and description are updated from the file. Its connection, target, schedules, sharing and notification settings are never touched.
Send dry_run: true to get the same plan without changing anything. That plan is the
drift between the file and the workspace:
{
"dry_run": true,
"changed": true,
"suite_fields": [],
"checks": [
{ "name": "status is a known value", "action": "update", "fields": ["config", "fail_threshold"] },
{ "name": "order id is never null", "action": "unchanged", "fields": [] }
],
"unmanaged": ["a check someone added in the app"]
}
fields names what differs and never shows a value, so a plan is safe in CI logs.
unmanaged checks do not count as drift unless you ask for prune.
Things to know:
- The whole file is validated before anything is written, so a file that is refused changes nothing. Each change is then made like an edit in the app, with its own version and audit entry. If something fails part-way, the earlier changes stay; apply again to finish.
- A threshold the file leaves out is cleared on the check, because the file is the whole definition.
- Renaming a check in the file reads as one new check and one unmanaged check. With
prunethe old one is deleted and its history goes with it. - A check's
kindcannot be changed. Give the new check a different name. - Names must be unique, in the file and among the suite's checks the file names.
dimension: nullcreates a new check unclassified, as import does. On a check that already exists it is treated as "leave it": apply cannot un-classify a check.- Only the checks apply would create or change are validated. A check the file leaves as it is stays, even if it is of a type that could no longer be created today.
- It needs edit access to the suite and runs with your own permissions.
From the command line¶
The dataq command wraps the three calls. A .yaml or .yml
file is sent as YAML; anything else is read as JSON.
dataq validate orders.yaml --connection "$CONNECTION_ID" # exit 2 if the file is invalid
dataq drift orders.yaml --suite "$SUITE_ID" # exit 2 if the suite differs
dataq apply orders.yaml --suite "$SUITE_ID" [--prune]
dataq export "$SUITE_ID" -o orders.yaml # start from what exists
In CI¶
Validate and report drift on every pull request, apply on merge. With GitHub Actions:
jobs:
suites:
runs-on: ubuntu-latest
env:
DATAQ_URL: ${{ vars.DATAQ_URL }}
DATAQ_PAT: ${{ secrets.DATAQ_PAT }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install "git+https://github.com/TheurgicDuke771/DataQ.git@main#subdirectory=packages/dataq-client"
- run: dataq validate suites/orders.yaml --connection "${{ vars.DATAQ_CONNECTION_ID }}"
- if: github.event_name == 'pull_request'
run: dataq drift suites/orders.yaml --suite "${{ vars.ORDERS_SUITE_ID }}" || true
- if: github.ref == 'refs/heads/main'
run: dataq apply suites/orders.yaml --suite "${{ vars.ORDERS_SUITE_ID }}"
The token is a personal access token of a user with edit access to the suite.
Import and export¶
| To | Call |
|---|---|
| Export as JSON | GET /api/v1/suites/{id}/export |
| Export as YAML | GET /api/v1/suites/{id}/export?format=yaml |
| Import JSON | POST /api/v1/suites/import with {connection_id, document} |
| Import YAML | POST /api/v1/suites/import with {connection_id, document_yaml} — the YAML as one text value |
| Apply or check drift | POST /api/v1/suites/{id}/apply with {document} or {document_yaml}, plus prune and dry_run |
Send document or document_yaml, never both. Import and validate need the Member role or
higher; export needs view access to the suite.
The format is covered by the API compatibility policy.