What it checks
A list of business rules, each one written as a condition that marks a row as breaking the rule. Policies must end after they start. Premiums must be positive. A claim cannot be dated before its policy begins. Most data teams keep a list like this somewhere, in a spreadsheet, a wiki page, or a governance tool. This template turns every rule on the list into its own child test.
A table usually has several rules, so a table gets several child tests. The rule id, not the table, is what tells the child tests apart: it goes in test.key. Add a rule to the file and the next materialization adds a child test. Retire a rule and its child test becomes a candidate for deletion.
Part of Template Test Examples: Import File. The same template with the rules returned by a script is Metadata Script Example: Business Rule Catalog.
The template
| Setting | Value |
|---|---|
| Name | Import File Example - Business Rule Catalog |
| Level | Table |
| Generation source | Import File |
| Severity | High |
| Quality dimension | Validity |
| Child test name | Rule {{var.rule_id}}: {{table.name}} - {{var.rule}} |
| Child folder | Relative, {{var.template_test_name}} |
| Test data set | Script on the template's data source, single value, numeric (below) |
| Control data set | Numeric value: {{var.max_violations}} |
| Value success criteria | Custom calculation: testNumericValue <= controlNumericValue |
| Overall success | Value, tolerance 0 |
| Missing result actions | Fail on both sides |
The test data set counts the rows that break the rule:
SELECT COUNT(*) AS VIOLATIONS
FROM {{schema.name}}.{{table.name}} t
WHERE {{var.violation_condition}}
The table is aliased t, so a condition can refer to its columns as t.COLUMN and can join out to other tables with EXISTS.
Variables declared on the template:
| Reference key | Type | Purpose |
|---|---|---|
rule_id |
Other | The rule's id from the catalog |
rule |
Other | The rule in plain language, used in the child test name and description |
violation_condition |
Other | SQL condition that is true for a row that breaks the rule |
max_violations |
Numeric | Violating rows allowed before the child test fails. Default 0 |
violation_condition is pasted into the WHERE clause as written. Handlebars runs with no escaping, so quotes, parentheses, and subqueries survive intact.
The import file
import-file-example-business-rule-catalog.xlsx, one sheet, header row plus 12 data rows.
| Header | Meaning |
|---|---|
schema.name, table.name |
The catalogued table the rule applies to |
test.key |
The rule id. Unique across the file |
var.rule_id |
The rule id again, for the child test name |
var.rule |
The rule in plain language |
var.violation_condition |
The condition that marks a violating row |
var.max_violations |
Allowed violations, 0 on every row |
The rules:
| Rule | Table | Rule | Violating rows are those where |
|---|---|---|---|
| BR-POL-01 | DIM_POLICY | Policy ends after it starts | t.POLICY_END_DATE < t.POLICY_START_DATE |
| BR-POL-02 | DIM_POLICY | Premium is positive | t.PREMIUM_AMOUNT IS NULL OR t.PREMIUM_AMOUNT <= 0 |
| BR-POL-03 | DIM_POLICY | Status is Active, Inactive or Overdue | t.POLICY_STATUS IS NULL OR t.POLICY_STATUS NOT IN ('Active', 'Inactive', 'Overdue') |
| BR-POL-04 | DIM_POLICY | Premium does not exceed the policy amount | t.PREMIUM_AMOUNT > t.POLICY_AMOUNT |
| BR-POL-05 | DIM_POLICY | Record is not modified before it is created | t.MODIFIED_DATE < t.CREATED_DATE |
| BR-CLM-01 | DIM_CLAIM | Claim amount is positive | t.CLAIM_AMOUNT IS NULL OR t.CLAIM_AMOUNT <= 0 |
| BR-CLM-02 | DIM_CLAIM | Claim type is a known type | t.CLAIM_TYPE IS NULL OR t.CLAIM_TYPE NOT IN ('Collision', 'Comprehensive', 'Liability', 'Personal Injury') |
| BR-CLM-03 | DIM_CLAIM | Claim date is not in the future | t.CLAIM_DATE > CURRENT_TIMESTAMP() |
| BR-CLM-04 | DIM_CLAIM | Claim is not dated before its policy starts | EXISTS (SELECT 1 FROM STAR_SCHEMA.DIM_POLICY p WHERE p.POLICY_ID = t.POLICY_ID AND t.CLAIM_DATE < p.POLICY_START_DATE) |
| BR-PAY-01 | FACT_PAYMENT | Payment amount is positive | t.PAYMENT_AMOUNT IS NULL OR t.PAYMENT_AMOUNT <= 0 |
| BR-PAY-02 | FACT_PAYMENT | Payment method is a known method | t.PAYMENT_METHOD IS NULL OR t.PAYMENT_METHOD NOT IN ('Bank transfer', 'Credit card', 'Debit card') |
| BR-PAY-03 | FACT_PAYMENT | Record is not modified before it is created | t.MODIFIED_DATE < t.CREATED_DATE |
DIM_POLICY appears five times, DIM_CLAIM four times, and FACT_PAYMENT three times. Each appearance has its own rule id in test.key, so each is its own child test.
What materializes
Twelve child tests, one per rule, in a folder named after the template. Rule BR-CLM-04 becomes the child test Rule BR-CLM-04: DIM_CLAIM - Claim is not dated before its policy starts, with this test script:
SELECT COUNT(*) AS VIOLATIONS
FROM STAR_SCHEMA.DIM_CLAIM t
WHERE EXISTS (SELECT 1 FROM STAR_SCHEMA.DIM_POLICY p WHERE p.POLICY_ID = t.POLICY_ID AND t.CLAIM_DATE < p.POLICY_START_DATE)
and a control value of 0.
Results on SAMPLE_DW
Verified 2026-10-19: 12 children, 10 pass, 2 fail.
| Rule | Violations | Result |
|---|---|---|
| BR-POL-01 to BR-POL-04 | 0 | Pass |
| BR-POL-05: DIM_POLICY record is not modified before it is created | 230 | Fail |
| BR-CLM-01 to BR-CLM-04 | 0 | Pass |
| BR-PAY-01, BR-PAY-02 | 0 | Pass |
| BR-PAY-03: FACT_PAYMENT record is not modified before it is created | 5,755 | Fail |
Both failures are the same rule on two tables: rows whose MODIFIED_DATE is earlier than their CREATED_DATE. Audit timestamps that run backwards usually mean a load process set one of them from a different clock or a different source. Because each table has its own child test, the two failures can be triaged and resolved separately.
Adapting it
Bring your own rule catalog. Export your rules to the five columns above and upload. A rule that spans tables belongs to the table it is measured on, with the other tables in an EXISTS or IN subquery.
Tolerate a known problem while it is fixed. Raise var.max_violations on that rule's row, upload, and materialize. Only that child test changes.
Keep the rule id stable. The rule id is the child test's identity. Renaming BR-POL-05 to BR-POL-5 deletes the old child test, with its result history, and creates a new one.
Report on rules, not tables. Every child test name starts with its rule id, so filtering test results by Rule BR-PAY lists one family of rules across every table.
Pitfall: the rule id in two places. test.key and var.rule_id hold the same value. test.key is stored in lower case, which is why the child test name uses the variable.
Pitfall: conditions that return NULL. t.PREMIUM_AMOUNT <= 0 is not true for a null premium, so a null would slip through. Add IS NULL OR when a null also breaks the rule, as the shipped rules do.