Data cleaning and preparation skill 07 of 7
Data validation rules
A validation rule encodes one assumption about your data so a machine catches the break before a stakeholder reads a wrong number. This skill makes an AI agent write the rules worth having, place each one in the right layer, set warn against block with care, and avoid the NULL traps that let bad data slip straight through.
data-validation-rules/SKILL.md, version 1.0.0, about 3,700 words.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 1 November 2026. Updated 1 November 2026. Free to use under CC BY 4.0.
When to use this, and when not to
Use this skill when you need to decide what to assert about a dataset and how to enforce it, so that a broken assumption is caught by a rule instead of by a stakeholder reading a wrong number. The triggers are a request to validate data, add data quality checks, write a schema or a data contract, set up Pandera, Great Expectations or dbt tests, add SQL constraints (CHECK, NOT NULL, UNIQUE, a foreign key), write a Pydantic model for incoming records, or work out why an existing test let a bad value through. Files that signal the work include a dbt schema.yml, a great_expectations/ directory, a Pandera schema, a Pydantic BaseModel, or a CREATE TABLE with constraints.
Do not use it to repair the values once a rule has flagged them. Filling gaps is handling-missing-data, repeated rows are duplicate-detection, extreme values are outlier-detection, messy strings are text-standardisation, dates are date-parsing, and wrong types are data-type-conversion. This skill flags and blocks; it does not clean. It is also not for validating a statistical experiment or a trained model (that is cross-validation and experiment design), nor for form validation inside an application beyond the data layer.
The core idea: a validation rule encodes one assumption about the data so a machine checks it on every load. The value of a rule is not that it runs, but that someone acts when it fails. So write the rules whose failure would actually cost you, place each where it belongs, make each one show the offending rows, and keep the set small enough that every failure gets looked at. A suite nobody reads is worse than no suite, because it manufactures the feeling of safety without the fact of it.
Inputs the agent must have before starting
- The schema and the meaning of each field. For every column you plan to check: its type, the range or set of allowed values, the format, and whether a blank is legal. A rule written without knowing what the column means will either miss real errors or reject good data.
- Which fields are critical. The primary key, the join keys, and the columns that feed money, health, legal or employment decisions. These get rules first; a wide, shallow suite on unimportant columns is how effort is wasted.
- Where the data is validated and with which tool. At the point of entry (SQL constraints), in the pipeline (dbt, Great Expectations, Pandera), or at an application boundary (Pydantic). The right rule and the right syntax depend on the layer.
- What should happen on failure. Whether a violation should block the pipeline or only warn, and who receives the alert. A rule with no owner and no consequence is noise.
- What is already validated upstream. If ingestion already asserts
not_nullon a key, repeating it in three transform models adds cost and no coverage. Ask before adding a rule that may already exist elsewhere.
If any of these is unknown, ask before writing rules. Guessing that a column is never null, or that a blocking failure is acceptable, either lets bad data through or halts a production pipeline on a false alarm.
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as data-validation-rules/SKILL.md.
---
name: data-validation-rules
description: >
Write, audit and place data validation rules that catch bad data before a human does, across pandas/Pandera, Great Expectations, dbt tests, Pydantic and SQL constraints. Covers the six quality dimensions (completeness, validity, uniqueness, consistency, accuracy, timeliness), which rules are worth writing, the layer each belongs in, severity (warn versus block), NULL semantics that silently defeat CHECK and UNIQUE, dynamic thresholds that avoid alert fatigue, and returning the offending rows. Use it when someone says "validate this data", "add data quality checks", "write a schema", "data contract", "Pandera schema", "Great Expectations suite", "dbt tests", "not_null", "accepted_values", "CHECK constraint", "my test didn't catch it", or "which checks should I add". Do NOT use it to fix bad values once found (see handling-missing-data, duplicate-detection, outlier-detection and the other cleaning skills), to validate a statistical experiment or model, or for app form validation beyond the data layer.
license: Free to use and adapt under CC BY 4.0, with credit to Afeez Adebayo. Link afeezadebayo.com, https://www.linkedin.com/in/afeez-adebayo/ and this skill's page at https://afeezadebayo.com/skills-md/data/data-cleaning-preparation/data-validation-rules/.
---
# Data validation rules
## When to use this, and when not to
Use this skill when you need to decide what to assert about a dataset and how to enforce it, so that a broken assumption is caught by a rule instead of by a stakeholder reading a wrong number. The triggers are a request to validate data, add data quality checks, write a schema or a data contract, set up Pandera, Great Expectations or dbt tests, add SQL constraints (`CHECK`, `NOT NULL`, `UNIQUE`, a foreign key), write a Pydantic model for incoming records, or work out why an existing test let a bad value through. Files that signal the work include a dbt `schema.yml`, a `great_expectations/` directory, a Pandera schema, a Pydantic `BaseModel`, or a `CREATE TABLE` with constraints.
Do not use it to repair the values once a rule has flagged them. Filling gaps is `handling-missing-data`, repeated rows are `duplicate-detection`, extreme values are `outlier-detection`, messy strings are `text-standardisation`, dates are `date-parsing`, and wrong types are `data-type-conversion`. This skill flags and blocks; it does not clean. It is also not for validating a statistical experiment or a trained model (that is cross-validation and experiment design), nor for form validation inside an application beyond the data layer.
The core idea: a validation rule encodes one assumption about the data so a machine checks it on every load. The value of a rule is not that it runs, but that someone acts when it fails. So write the rules whose failure would actually cost you, place each where it belongs, make each one show the offending rows, and keep the set small enough that every failure gets looked at. A suite nobody reads is worse than no suite, because it manufactures the feeling of safety without the fact of it.
## Inputs the agent must have before starting
1. The schema and the meaning of each field. For every column you plan to check: its type, the range or set of allowed values, the format, and whether a blank is legal. A rule written without knowing what the column means will either miss real errors or reject good data.
2. Which fields are critical. The primary key, the join keys, and the columns that feed money, health, legal or employment decisions. These get rules first; a wide, shallow suite on unimportant columns is how effort is wasted.
3. Where the data is validated and with which tool. At the point of entry (SQL constraints), in the pipeline (dbt, Great Expectations, Pandera), or at an application boundary (Pydantic). The right rule and the right syntax depend on the layer.
4. What should happen on failure. Whether a violation should block the pipeline or only warn, and who receives the alert. A rule with no owner and no consequence is noise.
5. What is already validated upstream. If ingestion already asserts `not_null` on a key, repeating it in three transform models adds cost and no coverage. Ask before adding a rule that may already exist elsewhere.
If any of these is unknown, ask before writing rules. Guessing that a column is never null, or that a blocking failure is acceptable, either lets bad data through or halts a production pipeline on a false alarm.
## The workflow, step by step
### 1. Choose what to validate from the cost of being wrong
Do not validate everything. Start from the question "what breaks, and who pays, if this value is wrong?" Put a uniqueness and a not-null rule on every primary key, which is the one assertion worth making on every table (dbt documentation). Then add rules where a violation would reach a dashboard, corrupt a model, or change a decision about a real person. Decision rule: if you cannot name what breaks when a rule fails, do not write that rule. "When everything's important, nothing is" (towardsdatascience, "Your data quality checks are worth less than you think").
### 2. Name the dimension each rule covers
Every rule belongs to one of the quality dimensions, and naming it keeps the suite balanced instead of ten variants of the same check (IBM, "What are data quality dimensions?"):
- **Completeness:** required values are present. Maps to `not_null`, the percentage of required fields populated.
- **Validity:** values follow the rules, formats and ranges. Maps to type checks, range checks (`price >= 0`), format or `pattern`, and `accepted_values`.
- **Uniqueness:** each record is distinct. Maps to `unique` and primary keys.
- **Consistency:** values agree across columns and systems (a `discount` never exceeds `price`; the same customer has one country).
- **Accuracy:** values match a trusted source. Usually a reconciliation against a system of record, not a cheap in-table check.
- **Timeliness:** data is current. Maps to a freshness check on a load timestamp and a volume check on row count.
Cover the dimensions that matter for the asset before adding depth to any one of them.
### 3. Place each rule in the earliest layer that can own it
- **Point of entry (SQL `CHECK`, `NOT NULL`, `UNIQUE`, `FOREIGN KEY`).** Enforced on every insert and update, so bad data never lands. The limit: a `CHECK` cannot reference other rows or other tables, so cross-row and cross-table logic must use `UNIQUE`, `FOREIGN KEY` or a trigger (PostgreSQL documentation, §5.5).
- **Pipeline (dbt, Great Expectations, Pandera).** Run at transform or read time against a batch. Best for cross-table relationships, aggregate checks, and anything the warehouse computes.
- **Application boundary (Pydantic).** Validates each record as it enters the system, before it is ever written.
Decision rule: validate each assumption once, at the earliest layer that can enforce it. Running the same `not_null` at ingestion and again in two transform models adds compute and maintenance without catching anything new (getdbt, "Understanding data validation"; towardsdatascience).
### 4. Make every rule return the offending rows
A rule that reports only pass or fail cannot be acted on. Write rules that surface the records that broke them: a dbt data test is a `select` that returns the failing rows, Great Expectations produces validation results per expectation, and Pandera with `lazy=True` raises `SchemaErrors` collecting every failure rather than stopping at the first. Turn on failing-row storage where the tool offers it (`dbt test --store-failures`) so debugging starts from the actual bad data, not from a red tick.
### 5. Get NULL semantics right, because this is where rules silently leak
The most common silent hole in a validation suite is a rule that looks correct and quietly ignores nulls.
- A SQL `CHECK` passes when its expression evaluates to NULL, not only when it is true. `CHECK (price > 0)` therefore accepts a NULL `price`, because `NULL > 0` is unknown. To forbid the null, add an explicit `NOT NULL` (PostgreSQL documentation).
- `UNIQUE` treats two NULLs as distinct by default, so several rows with a NULL in the constrained column are all allowed. Use `NULLS NOT DISTINCT`, or `NOT NULL`, when a single NULL is the most you want (PostgreSQL documentation).
- A foreign key under the default `MATCH SIMPLE` is not enforced for a row whose referencing column is NULL. Declare the column `NOT NULL` if the reference must always hold.
- In Pandera, a column rejects nulls unless you set `nullable=True`, so state nullability on purpose rather than relying on the default.
Decision rule: for every column, decide whether NULL is legal and write the rule that enforces that decision, because "greater than zero" and "not null" are two separate assertions and one does not imply the other.
### 6. Set severity deliberately: warn or block, with thresholds
Not every failure should stop the pipeline. Block on primary-key violations, broken referential integrity, and anything feeding money, health, legal or employment outcomes. Warn on softer signals such as a volume dip or a slow freshness breach, so the pipeline keeps moving while a human looks. dbt exposes this through `severity: warn` or `error`, and custom thresholds through `error_if` and `warn_if`; calibrate per rule rather than setting one global strictness (getdbt). Decision rule: a rule allowed to halt production must be one you would wake someone up for. Everything else warns.
### 7. Keep thresholds robust, not brittle
Static per-value thresholds are the main source of false positives, and a monitor that cries wolf gets silenced. In the Instagram ML study, a completeness-drop alert set at a fixed 30% produced so many false positives that engineers turned it off, and tuning per-feature thresholds took months and did not transfer between datasets (Shankar, Fawaz, Gyllstrom and Parameswaran, "Moving Fast With Broken Data", Meta and UC Berkeley). For volume and distribution checks, prefer a dynamic baseline, such as a z-score over a rolling window that absorbs weekday and seasonal patterns, over a hardcoded constant. Reserve hard limits for true invariants that cannot drift: a probability in [0, 1], a non-negative count, a date that cannot be in the future (IBM's validity example).
### 8. Deduplicate and prune the suite
A large suite is not a good suite. Review long-failing tests on a schedule and fix or remove them, because a wall of permanent red teaches people to ignore all of it (the broken-windows effect in data testing; mikkeldengsoe, "The State of Data Testing in 2024"). Route any alert a team cannot act on to a periodic report rather than a page. In dbt's 2024 analytics-engineering survey, 57% of respondents named alert overload, long-failing tests and poor data quality among their top obstacles, so fewer, sharper rules beat more rules.
### 9. Verify the rules themselves
A validation rule is code and can be wrong. Run each rule against known-good data and confirm it passes, then against a seeded violation and confirm it fails and names the bad rows. Check that NULLs behave the way you intended (step 5), that a blocking rule actually blocks, and that failing-row output works. Record coverage: which keys are covered, which dimensions each critical table has, and which rules are warn versus block. A rule that has never been shown to fail on bad data has not been tested.
## Worked example, end to end
An `orders` table needs validation before it feeds a revenue dashboard. The quick approach adds two rules and calls it done:
```sql
amount numeric CHECK (amount > 0),
order_id integer UNIQUE
```
This leaks in four ways. `CHECK (amount > 0)` passes a NULL `amount`, because `NULL > 0` is unknown. `UNIQUE` on `order_id` allows many rows with a NULL id, because NULLs are treated as distinct. `status` has no rule, so a typo like `"shippd"` flows straight to the dashboard. And nothing checks freshness, so a stalled load shows yesterday's revenue as if it were today's.
The corrected set names a consequence for each rule, places them at the staging layer, and stores failing rows. In dbt:
```yaml
models:
- name: stg_orders
columns:
- name: order_id
data_tests: [unique, not_null] # key: blocks; dedupe and joins depend on it
- name: amount
data_tests:
- not_null # completeness: blocks
- dbt_utils.accepted_range:
arguments: {min_value: 0} # validity: amount >= 0
- name: status
data_tests:
- accepted_values:
arguments:
values: ['placed','shipped','completed','returned'] # validity
- name: customer_id
data_tests:
- relationships:
arguments: {to: ref('stg_customers'), field: id} # consistency / integrity
data_tests:
- dbt_utils.recency:
config: {severity: warn} # timeliness: warn if loaded_at is stale
```
The same assumptions as a Pandera schema, for a pandas pipeline:
```python
import pandera.pandas as pa
schema = pa.DataFrameSchema(
{
"order_id": pa.Column(int, nullable=False, unique=True),
"amount": pa.Column(float, pa.Check.ge(0), nullable=False),
"status": pa.Column(str, pa.Check.isin(["placed","shipped","completed","returned"])),
"customer_id": pa.Column(int, nullable=False),
},
strict=True, # reject unexpected columns
coerce=False, # do not silently cast; a wrong type is a finding
)
schema.validate(df, lazy=True) # collect all failures, not just the first
```
Verifying, each rule fails on a seeded bad row and names it: a NULL `amount` now fails `not_null`, a duplicate `order_id` fails `unique`, `"shippd"` fails `accepted_values`, an `amount` of `-5` fails the range, and a stale load warns without blocking. Every rule is tied to a named consequence, placed once at staging, and the freshness check warns rather than halting a dashboard refresh over a late file.
## Variations you will meet in practice
- **Point of entry versus warehouse.** A transactional database can enforce `NOT NULL`, `CHECK` and `FOREIGN KEY` so bad data never lands, while a warehouse usually validates on read with dbt or Great Expectations. Push an invariant as far upstream as the architecture allows.
- **Record-level versus aggregate.** Spot-critical data such as financial postings or sensor readings may need record-level handling, for example a dead-letter queue or a flag on each bad row. For data that is only directionally important, such as hand-entered CRM fields, monitoring aggregate metadata (row counts, distinct key counts, min and max, unexpected enumerated values) costs far less and still catches process failures (towardsdatascience).
- **Cross-table integrity.** A `CHECK` cannot look at another table, so referential rules belong in a `FOREIGN KEY` or a dbt `relationships` test, not a column check (PostgreSQL documentation; dbt).
- **Nullable dtypes.** With pandas nullable types the marker is `pd.NA`, and a foreign key under `MATCH SIMPLE` is skipped when the referencing column is NULL; make such columns `NOT NULL` if the link must always hold.
- **Schema evolution.** Rules fall out of date as sources and business logic change. Keep validation code in version control and review it in pull requests like any other code, so a dropped column or a new status value updates the rule rather than silently breaking it (getdbt).
## Templates you can use verbatim
```sql
-- SQL point-of-entry constraints: note the explicit NOT NULL beside every CHECK
CREATE TABLE orders (
order_id integer PRIMARY KEY, -- unique + not null
customer_id integer NOT NULL REFERENCES customers (id), -- integrity, enforced because NOT NULL
amount numeric NOT NULL CHECK (amount >= 0), -- completeness + validity
status text NOT NULL CHECK (status IN ('placed','shipped','completed','returned')),
email text UNIQUE NULLS NOT DISTINCT, -- at most one NULL email
loaded_at timestamptz NOT NULL
);
```
```python
# Pydantic v2 record validation at an application boundary
from pydantic import BaseModel, Field, field_validator
from typing import Literal
class Order(BaseModel):
order_id: int
customer_id: int
amount: float = Field(ge=0) # validity: non-negative
status: Literal["placed","shipped","completed","returned"]
email: str | None = Field(default=None, pattern=r"^[^@]+@[^@]+\.[^@]+$")
@field_validator("amount")
@classmethod
def not_nan(cls, v: float) -> float:
if v != v: # NaN check
raise ValueError("amount must be a real number")
return v
```
```yaml
# Great Expectations: a small, named suite (expectation names are verifiable)
# expect_column_values_to_not_be_null(column="order_id")
# expect_column_values_to_be_unique(column="order_id")
# expect_column_values_to_be_between(column="amount", min_value=0)
# expect_column_values_to_be_in_set(column="status",
# value_set=["placed","shipped","completed","returned"])
```
## Failure modes and anti-patterns
1. **A CHECK that does not reject NULL, or a UNIQUE that allows duplicate NULLs.** The rule looks right and lets nulls through. Spot it by inserting a NULL into the constrained column and watching the rule pass. Fix: add `NOT NULL`, or `UNIQUE NULLS NOT DISTINCT`, alongside the check.
2. **Brittle static thresholds that cause alert fatigue.** A fixed limit fires on normal variation, so people mute the monitor and then miss the real break. Spot it when a check fails most days and everyone ignores it. Fix: use a dynamic baseline for volume and distribution, and keep hard limits only for true invariants.
3. **Duplicate rules across layers.** The same `not_null` runs at ingestion and in two transform models, adding cost and no coverage. Spot it by tracing one assumption through the pipeline. Fix: own each assertion once, at the earliest layer that can enforce it.
4. **Boolean-only rules.** The suite reports a failure but cannot show which rows broke, so no one can act. Spot it when "test failed" comes with nothing to investigate. Fix: write rules that return the failing rows and store them.
5. **Validating everything, or nothing tied to value.** Hundreds of shallow rules bury the few that matter, and users still find the real issues first. Spot it from high test counts and low trust. Fix: cover keys and decision-critical columns first, and tie every rule to a named consequence.
## Quality bar
- Every primary key has a uniqueness rule and a not-null rule.
- Each critical column states whether NULL is legal, and the rule enforces that decision rather than assuming it.
- Each rule is named with the dimension it covers and the consequence of its failure.
- Each assumption is validated once, at the earliest layer that can own it, with no duplicate across layers.
- Every rule returns or stores the offending rows, not just a pass or fail.
- Severity is set per rule: blocking rules are limited to key, integrity and high-stakes checks; everything else warns.
- Volume and distribution checks use a dynamic baseline, and hard limits are used only for true invariants.
- Each rule has been shown to pass on known-good data and fail, with the bad rows named, on a seeded violation.
## Data handling and safety
Validation reads the real data, which often includes personal or sensitive fields (email, income, health, identifiers). Do not log raw failing values to a shared dashboard or send them to a third-party service; store failures in a controlled location and mask sensitive columns in any report. Setting a rule to block a production pipeline affects other people's work and can halt a business process, so confirm with a human before making a rule blocking rather than warning, and never auto-delete or auto-quarantine rows a rule flags without explicit confirmation; default to flagging and reporting. When the data drives money, health, legal standing or employment, validation rules reduce error but do not replace domain review: add a human review step, and treat a passing suite as evidence the known assumptions held, not proof the data is correct.
## Appendix: tool-specific notes (verify before relying on these)
- **Pandera:** `DataFrameSchema({col: Column(dtype, checks, nullable, unique)})`; schema-level `checks`, `strict=True` to reject unexpected columns, `coerce` to cast (off by default here so a wrong type is a finding), `ordered` for column order, and `unique=[...]` for jointly unique columns. `validate(df, lazy=True)` runs all checks and raises `SchemaErrors` with every failure; without `lazy` it raises `SchemaError` at the first. Columns reject nulls unless `nullable=True`. Import as `pandera.pandas as pa`.
- **Great Expectations:** an Expectation is an assertion (for example `expect_column_values_to_not_be_null`); an Expectation Suite groups them; a Checkpoint runs a suite against a Batch in production and can fail the run; Validation Results and Data Docs record outcomes. Schema validation is necessary but not sufficient, so pair it with distribution and relationship checks.
- **dbt:** four built-in generic tests, `unique`, `not_null`, `accepted_values`, `relationships`; a data test is a `select` returning failing rows (zero rows passes); `severity: warn`/`error`, `error_if`/`warn_if` thresholds, and `--store-failures` to persist bad rows. Put `unique` and `not_null` on every model's primary key, and test source data before transforming it.
- **Pydantic v2:** `BaseModel` validates on instantiation; `Field(gt=, ge=, lt=, le=, min_length=, max_length=, pattern=)` for constraints; `@field_validator` (with `mode="before"` to run ahead of coercion) and `@model_validator(mode="after")` for cross-field rules; `ValidationError` on failure; `TypeAdapter` to validate lists or non-model data. Use `model_config` for `extra="forbid"` and strictness.
- **SQL:** `NOT NULL`, `UNIQUE` (with `NULLS NOT DISTINCT` to treat NULLs as equal), `PRIMARY KEY` (unique and not null), `CHECK` (passes when the expression is true or NULL, so pair with `NOT NULL`), and `FOREIGN KEY` for referential integrity (not enforced on NULL referencing columns under `MATCH SIMPLE`; index the referencing columns yourself). A `CHECK` cannot reference other rows or tables.
## Sources
- Pandera documentation, `DataFrameSchema` reference: constructor parameters (`columns`, `checks`, `coerce`, `strict`, `ordered`, `unique`, `report_duplicates`, `add_missing_columns`), `Check` on a `pd.Series`, `validate(..., lazy=True)` raising `SchemaErrors`, and `strict="filter"` behaviour. Verified 1 November 2026.
- dbt documentation, "Data tests": the four built-in generic tests, singular versus generic tests, a data test as a `select` returning failing rows, `severity`/`error_if`/`warn_if`, `--store-failures`, and the recommendation to test every primary key. Verified 1 November 2026.
- Great Expectations documentation, "Expectation Suite" (v0.18.21): Expectation, Expectation Suite, Batch, Checkpoint and Validator definitions, and suite storage as JSON. Verified 1 November 2026.
- PostgreSQL documentation, §5.5 "Constraints": `CHECK` passing when the expression is true or NULL, `NOT NULL`, `UNIQUE` default `NULLS DISTINCT` versus `NULLS NOT DISTINCT`, `PRIMARY KEY` implying unique and not null, and `FOREIGN KEY` behaviour under NULL referencing columns. Verified 1 November 2026.
- Pydantic v2 validation reference (field and model validators, `Field` constraints, `ConfigDict`): `BaseModel`, `Field(ge=, le=, min_length=, pattern=)`, `@field_validator` with `mode="before"`, `@model_validator(mode="after")`, and `ValidationError`. Verified 1 November 2026.
- IBM, "What are data quality dimensions?": definitions of accuracy, completeness, consistency, timeliness, validity and uniqueness, and how each is measured. Verified 1 November 2026.
- Shankar, Fawaz, Gyllstrom and Parameswaran, "Moving Fast With Broken Data" (Meta and UC Berkeley): a fixed 30% completeness-drop alert producing false positives that engineers silenced, per-feature thresholds taking months to tune and not transferring between datasets, that schema validation is necessary but not sufficient, and clustering correlated features and z-scores over rolling windows to cut false alarms. Verified 1 November 2026.
- mikkeldengsoe, "The State of Data Testing in 2024": the broken-windows effect of long-failing tests, that 57% of dbt's 2024 analytics-engineering survey respondents named alert overload and poor data quality among their top obstacles, and hygiene tests (`unique`, `not_null`, format, `accepted_values`) belonging in the staging layer. Verified 1 November 2026.
- towardsdatascience, "Your data quality checks are worth less than you think": testing each issue once, routing non-actionable alerts to reports, tying every check to a cost, monitoring aggregate metadata for tolerable data, and "when everything's important, nothing is". Verified 1 November 2026.
- getdbt, "Understanding data validation": validation as testing assertions about data, validating at multiple lifecycle points, keeping validation in version control, calibrating severity so some failures warn and others block, and the alert-fatigue risk of over-strict thresholds. Verified 1 November 2026.
- Existing implementations surveyed and improved on: anthropics/knowledge-work-plugins `data-validation` (a manual pre-delivery checklist with no executable rules or thresholds); wshobson/agents `data-quality-frameworks` (dimensions and tools, but a Great Expectations setup noted as verbose and a quick-start snippet that calls an undefined checkpoint); the `great-expectations-validator` and `data-quality-check` skill listings (tool-bound, no placement or severity guidance). This skill adds the which-rules-to-write decision rule, the layer-placement rule, the NULL-semantics traps, dynamic thresholds and the verify-the-rule step. Verified 1 November 2026.
The workflow, step by step
1. Choose what to validate from the cost of being wrong
Do not validate everything. Start from the question "what breaks, and who pays, if this value is wrong?" Put a uniqueness and a not-null rule on every primary key, which is the one assertion worth making on every table (dbt documentation). Then add rules where a violation would reach a dashboard, corrupt a model, or change a decision about a real person. Decision rule: if you cannot name what breaks when a rule fails, do not write that rule. "When everything's important, nothing is" (towardsdatascience, "Your data quality checks are worth less than you think").
2. Name the dimension each rule covers
Every rule belongs to one of the quality dimensions, and naming it keeps the suite balanced instead of ten variants of the same check (IBM, "What are data quality dimensions?"):
- Completeness: required values are present. Maps to
not_null, the percentage of required fields populated. - Validity: values follow the rules, formats and ranges. Maps to type checks, range checks (
price >= 0), format orpattern, andaccepted_values. - Uniqueness: each record is distinct. Maps to
uniqueand primary keys. - Consistency: values agree across columns and systems (a
discountnever exceedsprice; the same customer has one country). - Accuracy: values match a trusted source. Usually a reconciliation against a system of record, not a cheap in-table check.
- Timeliness: data is current. Maps to a freshness check on a load timestamp and a volume check on row count.
Cover the dimensions that matter for the asset before adding depth to any one of them.
3. Place each rule in the earliest layer that can own it
- Point of entry (SQL
CHECK,NOT NULL,UNIQUE,FOREIGN KEY). Enforced on every insert and update, so bad data never lands. The limit: aCHECKcannot reference other rows or other tables, so cross-row and cross-table logic must useUNIQUE,FOREIGN KEYor a trigger (PostgreSQL documentation, §5.5). - Pipeline (dbt, Great Expectations, Pandera). Run at transform or read time against a batch. Best for cross-table relationships, aggregate checks, and anything the warehouse computes.
- Application boundary (Pydantic). Validates each record as it enters the system, before it is ever written.
Decision rule: validate each assumption once, at the earliest layer that can enforce it. Running the same not_null at ingestion and again in two transform models adds compute and maintenance without catching anything new (getdbt, "Understanding data validation"; towardsdatascience).
4. Make every rule return the offending rows
A rule that reports only pass or fail cannot be acted on. Write rules that surface the records that broke them: a dbt data test is a select that returns the failing rows, Great Expectations produces validation results per expectation, and Pandera with lazy=True raises SchemaErrors collecting every failure rather than stopping at the first. Turn on failing-row storage where the tool offers it (dbt test --store-failures) so debugging starts from the actual bad data, not from a red tick.
5. Get NULL semantics right, because this is where rules silently leak
The most common silent hole in a validation suite is a rule that looks correct and quietly ignores nulls.
- A SQL
CHECKpasses when its expression evaluates to NULL, not only when it is true.CHECK (price > 0)therefore accepts a NULLprice, becauseNULL > 0is unknown. To forbid the null, add an explicitNOT NULL(PostgreSQL documentation). UNIQUEtreats two NULLs as distinct by default, so several rows with a NULL in the constrained column are all allowed. UseNULLS NOT DISTINCT, orNOT NULL, when a single NULL is the most you want (PostgreSQL documentation).- A foreign key under the default
MATCH SIMPLEis not enforced for a row whose referencing column is NULL. Declare the columnNOT NULLif the reference must always hold. - In Pandera, a column rejects nulls unless you set
nullable=True, so state nullability on purpose rather than relying on the default.
Decision rule: for every column, decide whether NULL is legal and write the rule that enforces that decision, because "greater than zero" and "not null" are two separate assertions and one does not imply the other.
6. Set severity deliberately: warn or block, with thresholds
Not every failure should stop the pipeline. Block on primary-key violations, broken referential integrity, and anything feeding money, health, legal or employment outcomes. Warn on softer signals such as a volume dip or a slow freshness breach, so the pipeline keeps moving while a human looks. dbt exposes this through severity: warn or error, and custom thresholds through error_if and warn_if; calibrate per rule rather than setting one global strictness (getdbt). Decision rule: a rule allowed to halt production must be one you would wake someone up for. Everything else warns.
7. Keep thresholds robust, not brittle
Static per-value thresholds are the main source of false positives, and a monitor that cries wolf gets silenced. In the Instagram ML study, a completeness-drop alert set at a fixed 30% produced so many false positives that engineers turned it off, and tuning per-feature thresholds took months and did not transfer between datasets (Shankar, Fawaz, Gyllstrom and Parameswaran, "Moving Fast With Broken Data", Meta and UC Berkeley). For volume and distribution checks, prefer a dynamic baseline, such as a z-score over a rolling window that absorbs weekday and seasonal patterns, over a hardcoded constant. Reserve hard limits for true invariants that cannot drift: a probability in [0, 1], a non-negative count, a date that cannot be in the future (IBM's validity example).
8. Deduplicate and prune the suite
A large suite is not a good suite. Review long-failing tests on a schedule and fix or remove them, because a wall of permanent red teaches people to ignore all of it (the broken-windows effect in data testing; mikkeldengsoe, "The State of Data Testing in 2024"). Route any alert a team cannot act on to a periodic report rather than a page. In dbt's 2024 analytics-engineering survey, 57% of respondents named alert overload, long-failing tests and poor data quality among their top obstacles, so fewer, sharper rules beat more rules.
9. Verify the rules themselves
A validation rule is code and can be wrong. Run each rule against known-good data and confirm it passes, then against a seeded violation and confirm it fails and names the bad rows. Check that NULLs behave the way you intended (step 5), that a blocking rule actually blocks, and that failing-row output works. Record coverage: which keys are covered, which dimensions each critical table has, and which rules are warn versus block. A rule that has never been shown to fail on bad data has not been tested.
Worked example, end to end
An orders table needs validation before it feeds a revenue dashboard. The quick approach adds two rules and calls it done:
amount numeric CHECK (amount > 0),
order_id integer UNIQUEThis leaks in four ways. CHECK (amount > 0) passes a NULL amount, because NULL > 0 is unknown. UNIQUE on order_id allows many rows with a NULL id, because NULLs are treated as distinct. status has no rule, so a typo like "shippd" flows straight to the dashboard. And nothing checks freshness, so a stalled load shows yesterday's revenue as if it were today's.
The corrected set names a consequence for each rule, places them at the staging layer, and stores failing rows. In dbt:
models:
- name: stg_orders
columns:
- name: order_id
data_tests: [unique, not_null] # key: blocks; dedupe and joins depend on it
- name: amount
data_tests:
- not_null # completeness: blocks
- dbt_utils.accepted_range:
arguments: {min_value: 0} # validity: amount >= 0
- name: status
data_tests:
- accepted_values:
arguments:
values: ['placed','shipped','completed','returned'] # validity
- name: customer_id
data_tests:
- relationships:
arguments: {to: ref('stg_customers'), field: id} # consistency / integrity
data_tests:
- dbt_utils.recency:
config: {severity: warn} # timeliness: warn if loaded_at is staleThe same assumptions as a Pandera schema, for a pandas pipeline:
import pandera.pandas as pa
schema = pa.DataFrameSchema(
{
"order_id": pa.Column(int, nullable=False, unique=True),
"amount": pa.Column(float, pa.Check.ge(0), nullable=False),
"status": pa.Column(str, pa.Check.isin(["placed","shipped","completed","returned"])),
"customer_id": pa.Column(int, nullable=False),
},
strict=True, # reject unexpected columns
coerce=False, # do not silently cast; a wrong type is a finding
)
schema.validate(df, lazy=True) # collect all failures, not just the firstVerifying, each rule fails on a seeded bad row and names it: a NULL amount now fails not_null, a duplicate order_id fails unique, "shippd" fails accepted_values, an amount of -5 fails the range, and a stale load warns without blocking. Every rule is tied to a named consequence, placed once at staging, and the freshness check warns rather than halting a dashboard refresh over a late file.
Variations you will meet in practice
- Point of entry versus warehouse. A transactional database can enforce
NOT NULL,CHECKandFOREIGN KEYso bad data never lands, while a warehouse usually validates on read with dbt or Great Expectations. Push an invariant as far upstream as the architecture allows. - Record-level versus aggregate. Spot-critical data such as financial postings or sensor readings may need record-level handling, for example a dead-letter queue or a flag on each bad row. For data that is only directionally important, such as hand-entered CRM fields, monitoring aggregate metadata (row counts, distinct key counts, min and max, unexpected enumerated values) costs far less and still catches process failures (towardsdatascience).
- Cross-table integrity. A
CHECKcannot look at another table, so referential rules belong in aFOREIGN KEYor a dbtrelationshipstest, not a column check (PostgreSQL documentation; dbt). - Nullable dtypes. With pandas nullable types the marker is
pd.NA, and a foreign key underMATCH SIMPLEis skipped when the referencing column is NULL; make such columnsNOT NULLif the link must always hold. - Schema evolution. Rules fall out of date as sources and business logic change. Keep validation code in version control and review it in pull requests like any other code, so a dropped column or a new status value updates the rule rather than silently breaking it (getdbt).
Templates you can use verbatim
-- SQL point-of-entry constraints: note the explicit NOT NULL beside every CHECK
CREATE TABLE orders (
order_id integer PRIMARY KEY, -- unique + not null
customer_id integer NOT NULL REFERENCES customers (id), -- integrity, enforced because NOT NULL
amount numeric NOT NULL CHECK (amount >= 0), -- completeness + validity
status text NOT NULL CHECK (status IN ('placed','shipped','completed','returned')),
email text UNIQUE NULLS NOT DISTINCT, -- at most one NULL email
loaded_at timestamptz NOT NULL
);# Pydantic v2 record validation at an application boundary
from pydantic import BaseModel, Field, field_validator
from typing import Literal
class Order(BaseModel):
order_id: int
customer_id: int
amount: float = Field(ge=0) # validity: non-negative
status: Literal["placed","shipped","completed","returned"]
email: str | None = Field(default=None, pattern=r"^[^@]+@[^@]+\.[^@]+$")
@field_validator("amount")
@classmethod
def not_nan(cls, v: float) -> float:
if v != v: # NaN check
raise ValueError("amount must be a real number")
return v# Great Expectations: a small, named suite (expectation names are verifiable)
# expect_column_values_to_not_be_null(column="order_id")
# expect_column_values_to_be_unique(column="order_id")
# expect_column_values_to_be_between(column="amount", min_value=0)
# expect_column_values_to_be_in_set(column="status",
# value_set=["placed","shipped","completed","returned"])Failure modes and anti-patterns
- A CHECK that does not reject NULL, or a UNIQUE that allows duplicate NULLs. The rule looks right and lets nulls through. Spot it by inserting a NULL into the constrained column and watching the rule pass. Fix: add
NOT NULL, orUNIQUE NULLS NOT DISTINCT, alongside the check. - Brittle static thresholds that cause alert fatigue. A fixed limit fires on normal variation, so people mute the monitor and then miss the real break. Spot it when a check fails most days and everyone ignores it. Fix: use a dynamic baseline for volume and distribution, and keep hard limits only for true invariants.
- Duplicate rules across layers. The same
not_nullruns at ingestion and in two transform models, adding cost and no coverage. Spot it by tracing one assumption through the pipeline. Fix: own each assertion once, at the earliest layer that can enforce it. - Boolean-only rules. The suite reports a failure but cannot show which rows broke, so no one can act. Spot it when "test failed" comes with nothing to investigate. Fix: write rules that return the failing rows and store them.
- Validating everything, or nothing tied to value. Hundreds of shallow rules bury the few that matter, and users still find the real issues first. Spot it from high test counts and low trust. Fix: cover keys and decision-critical columns first, and tie every rule to a named consequence.
Quality bar
- Every primary key has a uniqueness rule and a not-null rule.
- Each critical column states whether NULL is legal, and the rule enforces that decision rather than assuming it.
- Each rule is named with the dimension it covers and the consequence of its failure.
- Each assumption is validated once, at the earliest layer that can own it, with no duplicate across layers.
- Every rule returns or stores the offending rows, not just a pass or fail.
- Severity is set per rule: blocking rules are limited to key, integrity and high-stakes checks; everything else warns.
- Volume and distribution checks use a dynamic baseline, and hard limits are used only for true invariants.
- Each rule has been shown to pass on known-good data and fail, with the bad rows named, on a seeded violation.
Data handling and safety
Validation reads the real data, which often includes personal or sensitive fields (email, income, health, identifiers). Do not log raw failing values to a shared dashboard or send them to a third-party service; store failures in a controlled location and mask sensitive columns in any report. Setting a rule to block a production pipeline affects other people's work and can halt a business process, so confirm with a human before making a rule blocking rather than warning, and never auto-delete or auto-quarantine rows a rule flags without explicit confirmation; default to flagging and reporting. When the data drives money, health, legal standing or employment, validation rules reduce error but do not replace domain review: add a human review step, and treat a passing suite as evidence the known assumptions held, not proof the data is correct.
Appendix: tool-specific notes (verify before relying on these)
- Pandera:
DataFrameSchema({col: Column(dtype, checks, nullable, unique)}); schema-levelchecks,strict=Trueto reject unexpected columns,coerceto cast (off by default here so a wrong type is a finding),orderedfor column order, andunique=[...]for jointly unique columns.validate(df, lazy=True)runs all checks and raisesSchemaErrorswith every failure; withoutlazyit raisesSchemaErrorat the first. Columns reject nulls unlessnullable=True. Import aspandera.pandas as pa. - Great Expectations: an Expectation is an assertion (for example
expect_column_values_to_not_be_null); an Expectation Suite groups them; a Checkpoint runs a suite against a Batch in production and can fail the run; Validation Results and Data Docs record outcomes. Schema validation is necessary but not sufficient, so pair it with distribution and relationship checks. - dbt: four built-in generic tests,
unique,not_null,accepted_values,relationships; a data test is aselectreturning failing rows (zero rows passes);severity: warn/error,error_if/warn_ifthresholds, and--store-failuresto persist bad rows. Putuniqueandnot_nullon every model's primary key, and test source data before transforming it. - Pydantic v2:
BaseModelvalidates on instantiation;Field(gt=, ge=, lt=, le=, min_length=, max_length=, pattern=)for constraints;@field_validator(withmode="before"to run ahead of coercion) and@model_validator(mode="after")for cross-field rules;ValidationErroron failure;TypeAdapterto validate lists or non-model data. Usemodel_configforextra="forbid"and strictness. - SQL:
NOT NULL,UNIQUE(withNULLS NOT DISTINCTto treat NULLs as equal),PRIMARY KEY(unique and not null),CHECK(passes when the expression is true or NULL, so pair withNOT NULL), andFOREIGN KEYfor referential integrity (not enforced on NULL referencing columns underMATCH SIMPLE; index the referencing columns yourself). ACHECKcannot reference other rows or tables.
Sources
- Pandera documentation,
DataFrameSchemareference: constructor parameters (columns,checks,coerce,strict,ordered,unique,report_duplicates,add_missing_columns),Checkon apd.Series,validate(..., lazy=True)raisingSchemaErrors, andstrict="filter"behaviour. Verified 1 November 2026. - dbt documentation, "Data tests": the four built-in generic tests, singular versus generic tests, a data test as a
selectreturning failing rows,severity/error_if/warn_if,--store-failures, and the recommendation to test every primary key. Verified 1 November 2026. - Great Expectations documentation, "Expectation Suite" (v0.18.21): Expectation, Expectation Suite, Batch, Checkpoint and Validator definitions, and suite storage as JSON. Verified 1 November 2026.
- PostgreSQL documentation, §5.5 "Constraints":
CHECKpassing when the expression is true or NULL,NOT NULL,UNIQUEdefaultNULLS DISTINCTversusNULLS NOT DISTINCT,PRIMARY KEYimplying unique and not null, andFOREIGN KEYbehaviour under NULL referencing columns. Verified 1 November 2026. - Pydantic v2 validation reference (field and model validators,
Fieldconstraints,ConfigDict):BaseModel,Field(ge=, le=, min_length=, pattern=),@field_validatorwithmode="before",@model_validator(mode="after"), andValidationError. Verified 1 November 2026. - IBM, "What are data quality dimensions?": definitions of accuracy, completeness, consistency, timeliness, validity and uniqueness, and how each is measured. Verified 1 November 2026.
- Shankar, Fawaz, Gyllstrom and Parameswaran, "Moving Fast With Broken Data" (Meta and UC Berkeley): a fixed 30% completeness-drop alert producing false positives that engineers silenced, per-feature thresholds taking months to tune and not transferring between datasets, that schema validation is necessary but not sufficient, and clustering correlated features and z-scores over rolling windows to cut false alarms. Verified 1 November 2026.
- mikkeldengsoe, "The State of Data Testing in 2024": the broken-windows effect of long-failing tests, that 57% of dbt's 2024 analytics-engineering survey respondents named alert overload and poor data quality among their top obstacles, and hygiene tests (
unique,not_null, format,accepted_values) belonging in the staging layer. Verified 1 November 2026. - towardsdatascience, "Your data quality checks are worth less than you think": testing each issue once, routing non-actionable alerts to reports, tying every check to a cost, monitoring aggregate metadata for tolerable data, and "when everything's important, nothing is". Verified 1 November 2026.
- getdbt, "Understanding data validation": validation as testing assertions about data, validating at multiple lifecycle points, keeping validation in version control, calibrating severity so some failures warn and others block, and the alert-fatigue risk of over-strict thresholds. Verified 1 November 2026.
- Existing implementations surveyed and improved on: anthropics/knowledge-work-plugins
data-validation(a manual pre-delivery checklist with no executable rules or thresholds); wshobson/agentsdata-quality-frameworks(dimensions and tools, but a Great Expectations setup noted as verbose and a quick-start snippet that calls an undefined checkpoint); thegreat-expectations-validatoranddata-quality-checkskill listings (tool-bound, no placement or severity guidance). This skill adds the which-rules-to-write decision rule, the layer-placement rule, the NULL-semantics traps, dynamic thresholds and the verify-the-rule step. Verified 1 November 2026.
On this page
- When to use this skill, and when not to
- Who it is for
- What is inside
- Example prompts
- Example output
- What the agent needs from you
- How to install and use it
- How to write validation rules step by step
- Worked example
- Situations this skill handles
- What makes this skill different
- Common mistakes
- Related skills
- Frequently asked questions
- Changelog
When to use this skill, and when not to
Use this skill when you need to decide what to assert about a dataset and enforce it, so a broken assumption is caught by a rule and not by someone reading a wrong figure. It fires on a request to validate data, add data quality checks, write a schema or a data contract, set up Pandera, Great Expectations or dbt tests, add SQL constraints, or work out why a test let a bad value through. Do not use it to repair the values once flagged, which is the other cleaning skills, nor to validate a statistical experiment or a trained model.
Who it is for
Analysts, data and analytics engineers, and anyone using an AI agent to guard a dataset before it reaches a dashboard, a model or a decision. You do not need to know every tool, because the skill carries the decision rules for which checks to write, where to place them, and when a failure should warn rather than block.
What is inside
A nine-step workflow with decision rules, not vague advice. It covers choosing rules by the cost of being wrong, naming the quality dimension each rule serves, placing every rule in the earliest layer that can own it, returning the offending rows, the NULL semantics that silently defeat CHECK and UNIQUE, setting warn against block, dynamic thresholds that avoid alert fatigue, pruning duplicate rules, and verifying the rules against seeded bad data. It ends with a quality bar, a safety section and an appendix for Pandera, Great Expectations, dbt, Pydantic and SQL.
Example prompts
“Add data quality checks to this orders table.” “Write a Pandera schema for this dataframe.” “Why did my test miss the null amounts?” “Which dbt tests should I put on this model?” “Turn these column rules into SQL constraints.”
Example output
The skill returns a rule set with the reasoning stated, not just code. A shortened version: order_id gets unique and not_null because joins and dedupe depend on it; amount gets not_null beside the range check, because CHECK (amount >= 0) alone passes a null; status gets an accepted_values list so a typo is caught; a freshness check warns rather than blocks; and every rule is tied to what breaks if it fails, then placed once at the staging layer.
What the agent needs from you
The schema and what each column means, which fields are critical, where validation runs and with which tool, what should happen on a failure (warn or block, and who is alerted), and what is already validated upstream so a rule is not duplicated. The skill asks for these rather than guessing, because a rule written blind either misses real errors or rejects good data.
How to install and use it
In the Claude apps, download the SKILL.md from this page, put it in a folder named data-validation-rules, and upload that folder. In Claude Code, Codex and other Agent Skills tools, copy the folder into your skills directory. Then ask the agent to validate your data or add quality checks, and it loads the skill on its own.
How to write validation rules step by step
Install the skill as data-validation-rules/SKILL.md. Give the agent the schema, say which fields are critical and where validation runs, and confirm whether a failure may block the pipeline. The agent chooses rules by the cost of being wrong, names the dimension each covers, places each in the earliest layer that can own it, writes them to return the offending rows, fixes the NULL traps, sets warn against block, prefers dynamic thresholds for volume checks, and verifies each rule against a seeded violation.
Worked example
Asked to guard an orders table, the obvious move writes CHECK (amount > 0) and UNIQUE (order_id) and stops. That leaks in four ways: the check passes a null amount, the unique constraint allows many null ids, a typo in status flows straight through, and nothing catches a stalled load. The skill instead pairs each check with NOT NULL, adds an accepted_values list for status, a relationships test for customer_id, and a freshness check set to warn, with every rule tied to what it protects.
Situations this skill handles
| Situation | What the skill does |
|---|---|
| A primary key | Adds a uniqueness rule and a not-null rule, the one pair worth having on every table |
| CHECK (amount > 0) | Pairs it with NOT NULL, because a CHECK passes when the value is null |
| A status or category column | Adds an accepted_values list so a typo is caught as invalid |
| A foreign key relationship | Uses a relationships test or a FOREIGN KEY, which a CHECK cannot do |
| A volume or freshness signal | Uses a dynamic baseline set to warn, not a brittle fixed threshold |
What makes this skill different
Most guides hand you a tool and a list of checks. This skill adds the three judgements that decide whether a suite is worth anything: which rules to write, judged by what breaks if they fail; where each rule belongs, so the same assumption is not tested three times; and how tight to set it, so the monitor is not silenced for crying wolf. It also fixes the NULL traps that quietly defeat CHECK and UNIQUE, and it verifies each rule against seeded bad data before trusting it.
Common mistakes
Writing CHECK (x > 0) and assuming it rejects nulls. Leaving UNIQUE to allow duplicate nulls. Setting a fixed threshold that fires on normal variation until everyone mutes it. Running the same check at ingestion and again in transform. Writing a rule that reports pass or fail but cannot show the bad rows. The skill names each one with how to spot it and what to do instead.
Related skills
This skill flags bad data; the rest of the cleaning set fixes it. Pair it with handling-missing-data, duplicate-detection and outlier-detection for what to do once a rule fires, and with data-type-conversion, text-standardisation and date-parsing for the specific repairs. For what to do once the data is clean, see the data-analysis skills, including exploratory-data-analysis and descriptive-statistics.
Frequently asked questions
What is the difference between data validation and data cleaning?
Validation checks whether the data meets your rules and flags what does not; cleaning changes the values. This skill writes and places the rules. Once a rule fires, the sibling cleaning skills such as handling-missing-data and duplicate-detection decide how to fix what it found.
Which data validation tool should I use?
Use SQL constraints at the point of entry so bad data never lands, dbt or Great Expectations in the warehouse for cross-table and aggregate checks, Pandera for a pandas pipeline, and Pydantic for records entering an application. The skill places each rule in the earliest layer that can own it.
Why does my CHECK constraint let null values through?
A SQL CHECK passes when its expression is true or null, and most expressions are null when an operand is null, so CHECK (price > 0) accepts a null price. Add an explicit NOT NULL beside the check to forbid it.
Should a failed validation rule stop my pipeline?
Only for the rules you would wake someone up for: primary keys, broken referential integrity, and anything feeding money, health, legal or employment decisions. Softer signals such as a volume dip should warn, so the pipeline keeps moving while a person looks.
How do I stop data quality alerts from becoming noise?
Tie every rule to a named consequence, test each assumption once rather than at every layer, use dynamic baselines instead of fixed thresholds for volume checks, and route alerts a team cannot act on to a report rather than a page. A suite nobody reads is worse than none.
How many validation rules should a table have?
Start with a uniqueness and not-null rule on the primary key, then add rules only where a break would reach a dashboard, a model or a decision. Coverage of the critical columns beats a large suite of shallow checks that bury the few that matter.
Changelog
1.0.0, 1 November 2026: first release. Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
All Data Cleaning and Preparation skills
