Catalyst vs dbt tests: monitoring beyond your models
dbt tests are build-time assertions on the models dbt owns. Catalyst monitors any table on its own schedule, including the sources dbt never touches.
· 9 min read
dbt tests and Catalyst are not really competitors, and pretending otherwise would waste your time. dbt tests are assertions that run inside a transformation build, against the models dbt manages, at the moment dbt runs — which is exactly the right place to catch a join that fanned out or a primary key that stopped being unique. Catalyst is continuous monitoring that runs on a schedule against any table in your warehouse, including the raw sources and vendor-loaded tables dbt never touches, with a UI and run history that a non-engineer can use. If you already run dbt, keep your dbt tests; the question is what covers everything upstream and downstream of them.
What dbt tests actually are
A dbt test is a SQL query that is expected to return zero rows. That is the whole model, and its simplicity is why it works so well.
Four generic tests ship in dbt itself, declared in a schema.yml next to the model:
version: 2
models:
- name: orders
columns:
- name: order_id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values: ['pending', 'paid', 'shipped', 'refunded']
- name: customer_id
tests:
- relationships:
to: ref('customers')
field: id
dbt test compiles each of those into a select that returns the offending rows, runs it against the warehouse, and fails if anything comes back. Packages extend the vocabulary considerably — dbt_utils adds expression and combination-of-columns tests, dbt-expectations ports much of the Great Expectations catalogue into dbt's YAML — and singular tests let you drop a raw .sql file into tests/ to assert anything you can express in a query.
This is a genuinely good system. The tests live next to the model they describe, they are reviewed in the same pull request as the transformation, they run in CI, and they cost nothing beyond warehouse compute. If your data quality problem is "our transformations sometimes break", dbt tests solve it.
Where dbt tests stop
The limits are not defects. They follow directly from what dbt is: a transformation framework, not a monitoring product.
They run when dbt runs. A dbt test is an event in a build. If your build is nightly, your detection latency is a day — and if a table is loaded by Fivetran at 06:00 and dbt runs at 02:00, the check tells you about yesterday. Worse, if the build fails early or the orchestrator skips it, no tests run at all, and silence looks identical to success.
They cover models dbt manages. Sources can be tested too, and dbt source freshness is a useful check, but the coverage boundary is still the dbt project. The CRM export that lands in a staging schema, the finance spreadsheet someone uploads monthly, the replicated production database that three teams query directly — none of those are dbt models, and many of them are where incidents actually start.
There is no history and no UI. dbt test results exist in the run's logs and in run_results.json. Answering "has this check been failing intermittently for three weeks?" means parsing artifacts or standing up Elementary or dbt Cloud. Answering "which of our critical datasets are covered on freshness?" means grepping YAML. Failing rows can be materialised with store_failures, but then someone has to know the table exists and go query it.
Alerting is somebody else's job. dbt exits non-zero; Airflow, Dagster, GitHub Actions or dbt Cloud turn that into a notification. That notification usually says "the dbt job failed", not "the status column in orders grew a value nobody expected". Routing a specific failure to the person who owns that dataset is work you build yourself.
They are for people who write YAML in a repository. This is the constraint that decides most tooling conversations. The person who knows that a refund can never exceed the original order value is usually in finance, not in your dbt project. Their choices are to file a ticket or to not write the rule down, and most of the time it is the second one.
What Catalyst adds
Catalyst is a web application that connects read-only to your warehouse and monitors tables on a schedule you choose. Concretely, against the list above:
- It runs on its own cadence, independently of your build. Right after the load, hourly, daily — whatever matches how the data actually moves. A freshness check that fires because a load silently did not run is the single highest-value rule most teams are missing, and it cannot be caught by a build that also did not run.
- It monitors any table, whether dbt produced it or not: raw landing schemas, vendor-replicated tables, legacy reporting databases, and CSV, JSON or Excel uploads that never touch a pipeline at all. Connectors cover PostgreSQL, MySQL, SQL Server, BigQuery, Redshift and Microsoft Fabric, and the checks run as SQL in your warehouse — Catalyst stores results, violation counts and up to five example failing rows per check, never the dataset itself.
- It keeps run history. Every execution of every rule is stored, so a check that fails one morning in four is visible as a pattern rather than as four unrelated pages.
- It shows example failing rows in the interface. Click a failed rule, see up to five example rows that broke it, with no query to write and no artifact to find.
- It is usable without a repository. Import a table's schema, review the proposed baseline rules, adjust the ones that are wrong. Roles, SSO and an audit log mean you can let a data owner in finance edit their own thresholds without giving them commit access to anything.
- The rules are portable. Catalyst stores them as ODCS YAML contracts — the visual builder and the YAML are the same document, so a rule added in the UI is a one-line diff in an open standard you can take elsewhere.
Side by side
| dbt tests | Catalyst | |
|---|---|---|
| What it is | Assertions inside a transformation framework | Continuous data quality monitoring |
| When checks run | During a dbt build | On a schedule you set, independent of any build |
| Scope | Models and sources in the dbt project | Any table in a connected database, plus file uploads |
| Rule format | schema.yml tests, plus packages and singular SQL tests | ODCS YAML contracts, edited as YAML or visually |
| Who writes rules | Analytics engineers, in a repository | Anyone with access, in a browser |
| History | Logs and run_results.json | Stored run history per rule, with trends |
| Failing rows | store_failures into a table | Up to five examples per check, in the UI |
| Alerting | Via your orchestrator or dbt Cloud | Not at the time of writing |
| Access control | Git permissions | Roles, SSO, audit log |
| Cost | Free, plus warehouse compute | Free Starter tier, then per user — see pricing |
Stick with dbt tests alone when
There is a real set of teams for whom adding a second tool is overhead with no return. You are probably in it if most of these are true:
- Every dataset anyone makes decisions with is a dbt model. No side-loaded extracts, no direct queries against a replicated production database, no monthly spreadsheet.
- Your dbt build runs at least as often as your data changes, so build-time detection is not meaningfully slower than continuous monitoring.
- The people who know the business rules are the people who write dbt code. In a three-person data team this is often literally the same person, and a UI for non-engineers solves a problem you do not have.
- Your orchestrator already routes failures usefully, to the right channel, with enough context to act on.
- You have Elementary or dbt Cloud in place for test result history and you are satisfied with the visibility.
If that describes you, do not buy anything. Add dbt source freshness if you have not, set store_failures on the tests that matter, and get on with your work.
The list stops describing most teams at around the point where a second team starts depending on your data, or where the first incident originates in a table dbt has never heard of.
Running both: a sensible division of labour
Teams that use both usually settle on the same split, and it is worth stating plainly because it saves duplicated rules.
dbt tests own build-time correctness. Anything whose failure should stop a model from being published: uniqueness of the grain, not-null on keys, referential integrity between models, accepted values on enums you control, row count sanity after a join. Keep them in the repository, reviewed alongside the SQL they protect.
Catalyst owns the promise. Anything that describes what a dataset guarantees to its consumers, regardless of which pipeline wrote it today: freshness, completeness on business-critical columns, ranges that encode business rules, allowed values on fields owned by another system. These are the durable statements, they belong in a versioned contract, and they need checking whether or not a build ran.
Sources get monitored, not tested. Raw landing tables are where most incidents enter, and by definition they are the tables your transformation framework has least say over. Monitoring them on the load's own cadence is where continuous checking pays for itself fastest.
The practical starting point is not "migrate your dbt tests". It is: list the five tables your most-used dashboard depends on, note which of them dbt actually owns, and put monitoring on the ones it does not. That is a couple of hours of work, and it tends to find something in the first week. For the wider method, see the guide to validating your data.
Frequently asked questions
Does Catalyst replace dbt tests?
No, and it is not designed to. dbt tests are build-time assertions that should block a broken model from being published, and they are the right tool for that. Catalyst runs on a schedule against any table, including sources and datasets dbt does not manage, and adds run history, example failing rows and non-engineer access. Most teams that adopt Catalyst keep every dbt test they already had.
Can dbt tests monitor tables that dbt did not build?
Partially. You can declare an external table as a source and attach tests to it, and dbt source freshness checks how recently it was updated. The limitation is timing rather than scope: those checks still only run when dbt runs, so a source that breaks at 06:00 goes unreported until the next build. For tables outside the project entirely, or loaded by tools on their own schedule, scheduled monitoring detects the problem when it happens.
What is the difference between a dbt test and a data contract?
A dbt test is an assertion executed by a specific tool at a specific point in a pipeline. A data contract is a versioned description of what a dataset promises — schema, ownership, service levels and quality rules — written in a format independent of whatever runs the checks. Catalyst uses the Open Data Contract Standard, so the rules stay portable and reviewable as files rather than living inside one vendor's product.
How do I get alerted when a dbt test fails?
dbt exits with a non-zero status, and your orchestrator or dbt Cloud converts that into a notification. You build the routing yourself, and the message is usually about the job rather than the specific check. Catalyst does not send notifications at the time of writing: it runs each rule on its own schedule and records the outcome, so the dashboard shows which rule failed, on which dataset, how many runs it has been failing for, and up to five example rows that broke it.
Do I need to rewrite my dbt tests to use Catalyst?
No. Leave them where they are. Catalyst imports the schema of whichever tables you connect and proposes a baseline set of rules from the columns, keys and timestamps it finds, so you start by reviewing suggestions rather than by porting anything. In practice the rules you add are the ones dbt tests were never in a position to cover.