Dataobservability

AIRFLOW DATA QUALITY CHECKS

Airflow Data Quality Checks: Apache Airflow Data Quality Operators and SQL Check Monitoring Compared

Data quality monitoring that keeps running when the DAG does not, watching freshness, volume, schema, and distribution on every table your pipelines write. Read only connection, alerts in Slack and PagerDuty, from 99 dollars a month.

See pricing

14-day trial, no credit card, read-only connection

AIRFLOW · PROD
247 tables |
Break a monitor:

Alerted #data-eng 0.8s ago.

Downstream impact · consumers at risk

INCIDENT #1042 OPEN · owner @you

How do you run data quality checks in Airflow?

Airflow runs data quality checks through the SQL check operators in the common-sql provider: SQLColumnCheckOperator for column level assertions, SQLTableCheckOperator for whole table conditions, SQLCheckOperator for a single row of boolean results, SQLValueCheckOperator and SQLThresholdCheckOperator for comparisons against an expected value or range, and SQLIntervalCheckOperator for comparing a metric against its value some days back. You add them as tasks in a DAG, they run where you put them in the graph, and a failed check fails the task. Two things follow from that design. The results live in task logs rather than in a store you can query, so trend and history have to be built somewhere else, and a check that lives in a DAG cannot run when the DAG does not, which is the most common way bad data reaches production unnoticed. Dataobservability watches the tables themselves on a schedule of its own, so a pipeline that never started is an alert rather than silence, from 99 dollars a month.

Last updated August 2026

// COMPARE

Side by side

Airflow data quality checks compared

Swipe to see all columns →

Route to Airflow data quality What it catches What you build and operate Documented behavior and limits (Airflow docs, August 2026)
SQLColumnCheckOperator Per column assertions: null_check, distinct_check, unique_check, min and max, with conditions greater_than, geq_to, less_than, leq_to and equal_to A column_mapping for every column you want checked, on every table, kept in step with upstream schema changes equal_to is not compatible with other conditions. accept_none is true by default, which converts None values returned by the query to zeros, so a column that went entirely null can satisfy a min check. Only columns named in column_mapping are ever looked at
SQLTableCheckOperator Whole table conditions expressed as a check_statement that resolves to a boolean, including row counts and cross column arithmetic A checks dictionary per table, plus a partition_clause per check where you do not want to scan everything The check is whatever SQL you wrote, so the threshold in it is a number a person chose and now owns. There is no baseline and no seasonality, so a table that is quiet at the weekend needs the logic written by hand
SQLCheckOperator Anything you can express as a query returning a single row, where every value in the row is evaluated for truth The query, and care about what your SQL returns Values are evaluated using Python bool casting and the check fails if any value is falsy. False, 0, an empty string, an empty list and an empty dictionary all fail, so a count query that legitimately returns zero fails the check
SQLIntervalCheckOperator Drift in a metric against the same metric at an earlier date, using metrics_thresholds you supply A date_filter_column on the table and a threshold ratio per metric days_back defaults to -7 and date_filter_column defaults to ds, so the table needs a partition style date column. ratio_formula is max_over_min by default with relative_diff as the alternative, and ignore_zero defaults to true
SQLValueCheckOperator and SQLThresholdCheckOperator A query result compared against a pass_value with an optional numerical tolerance, or against a min_threshold and max_threshold that may themselves be queries One task per value you care about These compare one number at a time. Coverage is the count of tasks you wrote, which is why estates monitored this way tend to have deep coverage on a handful of tables and none at all on the rest
Great Expectations, Soda Core or dbt tests called from Airflow Richer assertion libraries with their own suites, expectations and result stores A second framework to install, configure, version and upgrade alongside Airflow, plus its result backend Astronomer documents these as the option to reach for when you want to collect the results of your data quality checks in a central place, which is the gap they exist to fill. GX Cloud was retired from public availability on June 1, 2026, and GX Core remains Apache 2.0
Dataobservability Freshness, volume, schema, and distribution breaks on every table your DAGs write, with lineage, grouped incidents and a run that does not depend on the DAG A read only connection, no agent Covers the connected warehouses (Snowflake, BigQuery, Databricks, Redshift) and dbt. Systems outside them are not monitored

Positioning and pricing models are summarized in good faith from each vendor's public pages, August 2026. Verify current terms with the vendor.

// CAPABILITY

What you get

What a monitoring layer adds on top of SQL check operators

Checks that still run when the DAG does not

This is the failure mode that DAG level checks structurally cannot cover, and Astronomer says so plainly in its own guidance: checks written inside a DAG cannot check data when there is a problem with Airflow. A scheduler that fell over, a DAG paused during an incident and never unpaused, a task that was skipped because an upstream sensor timed out, a deploy that removed the task by accident. In every one of those cases no check failed, because no check ran, and silence reads exactly like success on a dashboard. Monitoring that watches tables on its own schedule turns the absence of a load into the alert, which is the single most common real world data incident and the one a DAG can never raise about itself.

Coverage that is not the count of tasks somebody wrote

Every SQL check operator covers exactly what its arguments name. SQLColumnCheckOperator looks at the columns in column_mapping and no others, SQLTableCheckOperator evaluates the statements in its checks dictionary and nothing else. That is correct behavior for an assertion library and it means coverage is a manual inventory: new table, new task, new review. The tables that break are rarely the ones somebody sat down to instrument, they are the ones added six months ago by a team that has since reorganized. Reading the warehouse catalog and monitoring what is there makes coverage the default rather than a backlog item.

A history you can query instead of task logs

Airflow gives you full visibility of which checks failed from within task logs, which is genuinely useful in the moment and is not a trend. Answering how often this table has been late this quarter, whether the null rate has been creeping up for three weeks, or what the row count normally is on a Tuesday means the results have to land somewhere queryable, with a schema, retention and a way to chart them. That is why teams end up adding Great Expectations or Soda purely for its result store. Keeping a metric history per table by default removes that build entirely, and it is also what makes a baseline possible in the first place.

Baselines instead of numbers a person chose

SQLTableCheckOperator and SQLThresholdCheckOperator both need a threshold, and a threshold is a guess that ages. Set it tight and it fires every time the business has a good week, set it loose and it misses the load that delivered forty percent of the rows. SQLIntervalCheckOperator is the closest native answer, comparing against days_back, and its defaults tell you the shape of the assumption: minus seven days, a date column called ds, a max_over_min ratio. That works well for a daily partitioned fact table with clean seasonality and less well for everything else. Baselines learned from each table's own load history handle the weekend, the month end spike and the holiday without anybody encoding them.

Schema drift on columns no mapping mentions

A vendor adds a column, an upstream job retypes an amount from numeric to varchar, a field is dropped and a downstream view quietly resolves it to null. None of that fails a column_mapping that never named those columns, and the numeric retype is the nasty one because the check that reads the column may still pass while every aggregate downstream is now wrong. Watching the catalog for adds, drops and type changes is a different mechanism from asserting values, and it is the one that catches breakages nobody predicted, which by definition is most of them.

Alerting that knows the difference between new and ongoing

A failed check fails a task, and Airflow will tell you a task failed through whatever callback or notifier you configured. What it does not carry is incident state. A table stale since Friday fails the same task on every scheduled run, so the same alert arrives Saturday, Sunday and Monday, in the same channel, at the same severity, until somebody mutes the channel. Grouping related failures into one incident with an owner, an acknowledgement and a resolution is the difference between a page that gets answered and a notification rule that gets a filter written for it.

// 4 STEPS

How it works

From connected to caught

01

Connect the warehouse your DAGs write to, read only

Create a user with USAGE on the schemas and SELECT on the tables, and point Dataobservability at Snowflake, BigQuery, Databricks or Redshift. It reads catalog metadata and the warehouse's own statistics to build the table inventory and the freshness and volume picture, so nothing is copied out and no full scans are added to your bill. Nothing is installed in the Airflow deployment, no DAG is edited, and no provider is added to your requirements file, which matters because a monitoring layer that needs a deploy to change is a monitoring layer that will not change.

02

Let baselines build before you touch a threshold

Give it a few days of load history per table so weekly seasonality becomes visible. Tuning before a baseline exists is how teams manufacture alert fatigue in week one and stop trusting the monitoring by week two. Leave your existing SQL check operator tasks running during this period and compare what each one flags. They fail differently and that is the useful part: the operators will catch the business rule violations you encoded, and the baselines will catch the loads that did not happen at all.

03

Keep SQL check operators for the invariants

The operators are good at exactly what a learned baseline cannot know, and you should not delete them. That a discount percentage never exceeds 40, that every order carries a valid region code, that a status column only ever holds one of five values, that a foreign key resolves. Those are business rules, they belong next to the transformation that produces them, and SQLColumnCheckOperator with a well chosen column_mapping is a clean way to express them. Leave outages, drift, volume anomalies and schema changes to the layer that watches continuously. That split is what keeps a DAG readable instead of growing a check task per column until nobody can see the pipeline.

04

Route by dataset owner, then prune what never fires

Send alerts to the team that owns each schema rather than into one shared data alerts channel that everybody has muted. After two weeks, read the log back honestly: a monitor that has never fired on a real problem should be relaxed or removed, and any incident that reached you through a complaining stakeholder should become a monitor. Monitoring that is never pruned turns into noise, and noise gets muted, usually by the person you most needed to reach.

The SQL check operators, precisely what each one does

Airflow ships its data quality tooling in the common-sql provider, and the six operators there are more different from each other than most write ups suggest. SQLColumnCheckOperator is the one most teams start with. You give it a table, a connection and a column_mapping, a dictionary describing each column and the tests to run against it. The available checks are null_check, distinct_check, unique_check, min and max, and each takes a condition from greater_than, geq_to, less_than, leq_to and equal_to. Two details in the documentation matter more than they look. First, equal_to is not compatible with other conditions, so you cannot ask for a value equal to something and bounded by something else in the same check. Second, accept_none is true by default, and it converts None values returned by the query to zeros. Think about what that means for a min check on a column that has gone entirely null after an upstream change: the query returns None, the operator reads zero, and if your condition is that the minimum should be greater than or equal to zero, the check passes on a column with no data in it. There is also a tolerance parameter, documented as a percentage that the result may be out of bounds but still considered successful, and a partition_clause you can set at operator, column or check level to avoid scanning the whole table. SQLTableCheckOperator moves up a level, taking a checks dictionary where each entry carries a check_statement containing a SQL statement that resolves to a boolean. That is the general purpose escape hatch: row counts, cross column arithmetic, anything you can phrase as a condition. SQLCheckOperator is the oldest and the sharpest edged. It runs a query that returns a single row and evaluates every value in that row using Python bool casting, failing if any value is falsy. The documented falsy set includes False, zero, an empty string, an empty list and an empty dictionary. The trap is zero. A query written as a count of bad rows returns zero when everything is fine, and zero is falsy, so the check fails precisely when the data is good. You write it as a comparison returning a boolean instead, and everyone learns this once, usually at three in the morning.

SQLIntervalCheckOperator and the defaults that decide whether it fits you

SQLIntervalCheckOperator is the only native operator that compares a metric against history rather than against a constant, which makes it the closest thing Airflow has to anomaly detection and worth understanding exactly. You give it a table and a metrics_thresholds dictionary mapping metric names to an acceptable ratio, and it compares the metric today against the metric at an earlier date. The defaults describe the assumption it is built on. days_back defaults to minus seven, so out of the box it compares against the same day last week, which is a sensible choice because it holds day of week constant. date_filter_column defaults to ds, the conventional partition date column name, which means the operator expects a table partitioned or at least filterable by a date column with that name, and tables that do not have one need the parameter set. ratio_formula defaults to max_over_min, which divides the larger value by the smaller and so treats a doubling and a halving identically, with relative_diff available as the alternative when you want a signed proportional difference. ignore_zero defaults to true, which skips the comparison when a value is zero rather than treating it as an infinite ratio. Put together, this is a competent week over week drift check for a daily partitioned fact table with a conventional date column. What it is not is a baseline. It knows one prior data point, not a distribution, so it cannot tell you that today is unusual relative to the last ninety days, it cannot learn that the first of the month is always triple, and it cannot express a confidence interval. It compares two numbers and applies a ratio you chose. For a table whose volume genuinely does vary, choosing that ratio is the whole problem, and it is the problem the operator hands back to you.

Why the checks stop when the DAG stops, and why that is the incident you care about

Astronomer, the company that employs a large share of Airflow committers, publishes a comparison of DAG level and platform level data quality in its own documentation, and the first line of that comparison is the honest one: DAG level checks cannot check data when there is a problem with Airflow, while platform level checks monitor data even if there is a problem with Airflow. That sentence is worth sitting with, because it describes the majority of real data incidents rather than an edge case. Rank the ways a table goes wrong in production and the list starts with the load not running at all: the scheduler was down, the DAG was paused during a deploy and nobody unpaused it, a sensor timed out and the downstream tasks were skipped, a task was accidentally removed in a refactor, the connection credentials expired, someone cleared a task and forgot to rerun it. In every one of those cases the data is stale and wrong, and no data quality check failed, because the tasks that contain the checks did not execute. A green DAG list and an empty alert channel look identical to a working pipeline and a broken one that never started. The second structural limit from the same comparison is deployment friction: DAG level checks require DAG code changes and deployment, and require DAG authoring expertise, where a platform is configured through an independent interface. That sounds like a convenience argument and it is really a coverage argument. If adding a check means a pull request, a review from someone who knows the DAG, and a deploy window, then checks get added to the tables that already have somebody paying attention, and the long tail stays uncovered forever. The third is lineage. Astronomer describes DAG level lineage as what you get from checks inside the DAG, against table level lineage from a platform. Airflow does emit lineage through the OpenLineage provider, but it is not bundled with Airflow, it is installed separately as apache-airflow-providers-openlineage and supports Airflow 2.11.0 and later, and what it captures depends on the operators in your DAG having support. None of this makes the operators a bad choice. It makes them a complement to something watching the tables, rather than a substitute for it.

Where third party frameworks fit, and the result store nobody scopes

The usual next step, once SQL check operators have been in place for a quarter, is to reach for Great Expectations, Soda Core or dbt tests, and the reason is almost always the same. Astronomer names it directly in its guidance on when to use a third party framework: you use one when you want to collect the results of your data quality checks in a central place. Airflow does not store check results as data. A check runs, it passes or fails, the detail lands in the task log, and the task instance carries the pass or fail. That is enough to debug this morning and not enough to answer a question with the word trend in it. How often has this table been late this quarter. Is the null rate on this column drifting. What is the normal row count on a Tuesday, and is today outside it. Each of those needs the results written somewhere with a schema, retained, and charted, and that store is a real piece of infrastructure with a migration path and an owner. It is common to see teams adopt an entire assertion framework mainly to inherit its result backend, which is a lot of surface area to take on for a table. The frameworks bring real benefits beyond that, particularly richer expectation libraries and profiling, and they also bring their own dependency and upgrade path inside your Airflow image, and their own learning curve. It is worth knowing the state of that ecosystem before choosing. Great Expectations is now stewarded by Fivetran, GX Core remains Apache 2.0 and freely usable, and GX Cloud was retired from public availability on June 1, 2026 after being acquired by FICO, so the hosted option that used to solve the result store problem is no longer on the table for new adopters. Soda publishes a price, at 750 dollars a month for its Team plan with a free tier below it, which makes it one of the few in this space you can evaluate without a sales call. dbt tests are the natural fit if your transformations are already dbt models, and they inherit the same shape of limitation, since a test that runs as part of dbt build does not run when dbt build does not.

Build the layer above Airflow or buy it

The honest answer is that a lot of teams should not buy anything. If you have a few dozen tables that genuinely matter, they all flow through DAGs an engineer maintains, and the business rules are more important to you than outage detection, then SQLColumnCheckOperator and SQLTableCheckOperator on those tables are the right amount of tooling, they cost nothing, and you should stop reading. The line moves when coverage becomes the goal rather than a curated list, and it moves fast, because what you are building at that point is not a set of checks, it is a system. Something to watch tables independently of the scheduler so a load that never ran raises an alert. A result store with a schema and retention so history exists. Baselines per table so thresholds stop being guesses, and seasonality so weekends do not page anybody. A catalog watcher for schema changes, since column_mapping never sees a column it was not told about. Incident state so a table stale for three days does not re alert every run. An ownership map and routing into Slack or PagerDuty. That work is not difficult, which is exactly why it gets underestimated, and it is funded by the most senior data engineer available, taken directly out of the pipeline work they were hired for. This is also why published pricing matters more in this category than in most. Of the thirteen observability tools we track, three publish a price: Soda at 750 dollars a month for its Team plan, Metaplane, and us. Monte Carlo publishes no list price on its own site, though its AWS Marketplace listing shows a 12 month contract at 50,000 dollars for its credit dimension, with overage at one cent per unit. Bigeye pricing page returns a 404, while its AWS Marketplace listing shows 45,000 dollars a year for 100 actively monitored tables and 75,000 dollars for 300. Datafold redirects its pricing page to a contact form. All three figures were checked directly on the listings in August 2026. Dataobservability is 99, 299, and 799 dollars a month, with a 14 day trial that needs no credit card, so you can hold a real number against the engineering estimate before anyone books a call.

// FAQ

Questions buyers ask

Airflow data quality checks FAQ

What are the SQL check operators in Airflow?

They are the data quality operators in the common-sql provider: SQLColumnCheckOperator for per column assertions, SQLTableCheckOperator for whole table conditions, SQLCheckOperator for a query returning a single row of boolean values, SQLValueCheckOperator for comparing a result against an expected value, SQLThresholdCheckOperator for comparing against a minimum and maximum, and SQLIntervalCheckOperator for comparing a metric against its value some days back.

What is the difference between SQLColumnCheckOperator and SQLTableCheckOperator?

SQLColumnCheckOperator runs predefined checks against named columns using a column_mapping, choosing from null_check, distinct_check, unique_check, min and max with conditions such as greater_than and less_than. SQLTableCheckOperator runs checks against the table as a whole, where each check carries a check_statement containing arbitrary SQL that resolves to a boolean. Use the first for column properties and the second for row counts and cross column logic.

How does SQLCheckOperator decide whether a check passed?

It runs a query that returns a single row and evaluates every value in that row using Python bool casting, failing the task if any value is falsy. The documented falsy values include False, zero, an empty string, an empty list and an empty dictionary. This is why a query written as a count of bad rows is a bug: it returns zero when the data is clean, and zero fails the check. Return a boolean comparison instead.

What does SQLIntervalCheckOperator compare?

It compares metrics against their values at an earlier date, using a metrics_thresholds dictionary that maps each metric to an acceptable ratio. days_back defaults to minus seven, so it compares against the same day last week, and date_filter_column defaults to ds. ratio_formula is max_over_min by default, with relative_diff as the alternative, and ignore_zero defaults to true so zero valued metrics are skipped rather than producing an infinite ratio.

Does Airflow store the results of data quality checks?

Not as queryable data. A check passes or fails, the detail goes to the task log, and the task instance records the outcome. That is enough to debug an incident this morning and not enough to answer anything about trends, which is why Astronomer points teams at a third party framework when they want to collect check results in a central place. Any question containing the words normally, drifting or how often needs a result store you build or buy.

What happens to Airflow data quality checks if the DAG never runs?

Nothing happens, and that is the problem. A check written as a task cannot run when the task does not, so a paused DAG, a failed scheduler, a skipped branch or a task removed in a refactor all produce silence rather than an alert. Astronomer states the limit directly, that DAG level checks cannot check data when there is a problem with Airflow. Monitoring the tables on an independent schedule is what turns a load that never happened into an alert.

Do I need Great Expectations or Soda with Airflow?

Only if you need what they add beyond assertions, which is usually a central result store, richer expectation libraries and profiling. Both run from Airflow as tasks and both inherit the same limit, that they do not run when the DAG does not. Note that GX Cloud was retired from public availability on June 1, 2026, so GX Core, which is Apache 2.0, is the freely available option, and Soda publishes a Team plan at 750 dollars a month.

Does Airflow have column level lineage?

Not natively, and not bundled. Airflow emits lineage through the OpenLineage provider, which is installed separately as apache-airflow-providers-openlineage and supports Airflow 2.11.0 and later, and what it captures depends on the operators in your DAG having support for it. Astronomer describes what DAG based checks give you as DAG level lineage, against table level lineage from a platform, so mapping a broken column to the dashboards downstream of it is a separate capability.

What does the accept_none parameter in SQLColumnCheckOperator do?

It is true by default and converts None values returned by the query into zeros. That is convenient and it has a sharp edge: if a column goes entirely null after an upstream change, an aggregate such as min returns None, the operator reads it as zero, and a condition like greater than or equal to zero passes on a column with no data in it. Set it to false when a null result should be treated as a failure rather than a number.

How do you alert on a failed Airflow data quality check?

Through Airflow task failure handling, meaning callbacks or notifiers configured on the task or DAG, which delivers a task failed message to wherever you point it. What that does not carry is incident state, so a table stale since Friday fires the same alert on every scheduled run until somebody fixes it or mutes the channel. Grouping repeated failures into one incident with an owner and an acknowledgement is a layer above what the scheduler provides.

Catch broken data before your stakeholders do

Connect your warehouse and get airflow data quality checks live from one read-only connection. Transparent pricing, no credit card.