# Data quality checks

Three checks run on every sync. Two you get without asking, and one you write in the contract.

## Overview

A run that reports success has not necessarily moved your data correctly. It may have extracted from a source that quietly changed shape, or landed fewer records than it read, or loaded data that is structurally perfect and factually wrong.

Those are three different failures, and no single check catches all three. Amfelt runs three, at three points in a sync. Two of them you get without configuring anything.

## What runs, and when

| Check | When it runs | You configure it | If it fails |
| --- | --- | --- | --- |
| Schema validation | As data is extracted | No | Warns |
| Count reconciliation | After data lands in staging | No | Warns |
| Rules | After load, against the landed table | Yes, in the contract | Warns, or fails the run |

### Schema validation

As the agent reads from the source, it compares the shape the source actually sent against the shape the contract declares: whether every expected field is present, whether types still match at the logical level, whether columns declared required are still required, and whether the primary key is intact.

This catches a source that changed between discovery and this run. It is warn-only: it tells you the contract and the source have diverged, and leaves what to do about it to [drift review](/docs/guides/catalog/review-schema-drift/), where the change is classified.

### Count reconciliation

Once a table lands in staging, the agent counts what actually arrived and compares it against what it read from the source. The gap is recorded as a signed number on the run; zero means clean.

The count is taken independently, by querying the landed table, rather than by trusting what the load reported about itself. That distinction is the whole value of the check: a load that miscounts its own writes will report success either way, and only an independent count disagrees with it.

This is what catches silent record loss (records the target rejected or coerced away), which would otherwise produce a run that looks identical to a clean one. It runs on the staging layer, where every run rewrites the table and the arithmetic is unambiguous.

### Rules

The first two checks verify that the data arrived intact. Neither has any opinion about whether the data is *right*. A column of perfectly-typed, fully-transferred negative order totals passes both.

Rules are where you say what right means. They are declared on the data contract as quality rules, each a query and an expected result: this count must be zero, this value must fall between these bounds. After a table lands, each rule runs against it and its result is compared against what you declared.

Rules are the only check that can fail a run. A violation warns by default; a rule you have marked critical fails the run outright, and the failed step names the rule, the table it ran on, and the value its query returned. Critical is the lever for the distinction between something you want to know about and something the run should fail on. It is not a gate: a rule runs after the data has landed, and the run still goes on to the layers after staging for that object.

Because a rule is SQL you wrote and Amfelt runs, it executes inside a read-only database transaction. A rule that attempts to modify anything is rejected by the database itself rather than by an inspection of the query text, so the guarantee holds regardless of how the query is written. Amfelt cannot yet open a read-only transaction on DuckDB, so on a DuckDB target each rule is reported as not evaluated instead of being run.

## Choosing what to write

The two automatic checks need nothing from you and cover the mechanical failures: the source changed, or the records did not all arrive.

Rules are worth writing where you know something about the data that no schema can express: a status column that should only ever hold four values, a total that should never be negative, a foreign key that should always resolve. Start with the assumptions a downstream consumer is already making silently, and make the important ones critical.

## Next

- [Review the data contract](/docs/guides/sources/review-the-data-contract/): where rules are declared
- [Review schema drift](/docs/guides/catalog/review-schema-drift/): what happens when validation reports a divergence
- [Versioning and compatibility](/docs/concepts/versioning/): how a divergence is scored
