dbt Import
Convert an existing dbt test suite into Qualytics quality checks. Point the CLI at a compiled manifest.json. Conversion happens in memory and the checks are created or updated in a datastore.
What gets imported?
Every generic test (not_null, unique, accepted_values, relationships, plus the dbt_utils and dbt_expectations packages) and every singular SQL test in the manifest becomes a quality check. Nothing is skipped: tests without a direct rule mapping arrive as satisfiesExpression drafts carrying their dbt source, ready for a reviewer to finish. See Quality Checks for how checks work in general.
Commands
| Command | Description |
|---|---|
dbt plan |
Preview what a manifest would import as (offline, no authentication) |
dbt import |
Convert a manifest and create or update the checks in a datastore |
Workflow
graph LR
D[dbt compile] --> M[manifest.json]
M -->|dbt plan| P[Preview tiers and mappings]
M -->|dbt import| Q[(Qualytics datastore)]
- Compile your dbt project.
dbt compilewritestarget/manifest.json. - Run
qualytics dbt planto preview, thenqualytics dbt importto apply.
Only manifest.json is needed. It carries no database credentials (those live in profiles.yml, which dbt never compiles into the manifest), but it does include compiled SQL and full schema lineage, so treat it like source code.
Plan
Preview the import before touching anything. plan reads the manifest locally, so it works offline and in CI without credentials.
qualytics dbt plan --manifest target/manifest.json
qualytics dbt plan --manifest target/manifest.json --show-checks
Plan
• All 26 dbt tests convert to 28 Qualytics checks.
• 25 (89%) map to a rule automatically.
• 3 (11%) need an expression authored by hand.
▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌▌
Tier Checks Status
direct 19 Active maps 1:1 to a Qualytics rule
normalize 6 Draft mapped, but a parameter needs review
manual 3 Draft custom SQL, authored by hand
Note: some dbt tests translate to more than one Qualytics check.
| Option | Type | Default | Required | Description |
|---|---|---|---|---|
--manifest, -m |
TEXT | target/manifest.json |
No | Path to the compiled dbt manifest.json |
--container-map |
TEXT | - | No | Override a container name: model=container (repeat for multiple) |
--container-case |
TEXT | - | No | Force container name case: upper or lower |
--show-checks |
FLAG | false |
No | List every dbt test and the rule it maps to |
Use --show-checks to see the full mapping for your project
The per-test listing shows each dbt test alongside the Qualytics rule it becomes and its tier, so you can spot anything that needs hand-finishing before you import.
Import
Convert the manifest and create or update the checks in one or more datastores. Unlike plan, import talks to your Qualytics instance, so it needs authentication and a synced datastore.
# Preview
qualytics dbt import --manifest target/manifest.json --datastore-id 42 --dry-run
# Apply
qualytics dbt import --manifest target/manifest.json --datastore-id 42
# Also write the converted YAML for git review
qualytics dbt import --manifest target/manifest.json --datastore-id 42 \
--emit-yaml ./qualytics-config/checks/
| Option | Type | Default | Required | Description |
|---|---|---|---|---|
--datastore-id |
INTEGER | - | Yes | Target datastore ID (repeat the flag for multiple) |
--manifest, -m |
TEXT | target/manifest.json |
No | Path to the compiled dbt manifest.json |
--dry-run |
FLAG | false |
No | Preview what would be created or updated |
--container-map |
TEXT | - | No | Override a container name: model=container (repeat for multiple) |
--container-case |
TEXT | - | No | Force container name case: upper or lower |
--status |
TEXT | - | No | Force every check to Active or Draft, overriding the tier default |
--preserve-status |
FLAG | false |
No | Omit status so re-imports keep what is set in the platform |
--validate-fields / --no-validate-fields |
FLAG | --validate-fields |
No | Check field names against the synced schema and correct their casing |
--emit-yaml |
TEXT | - | No | Also write the converted checks to this directory |
--failures-log |
TEXT | dbt-import-failures.log |
No | Write checks that failed to import (which test, why) to this file |
--status and --preserve-status are mutually exclusive.
Conversion tiers
Every dbt test converts. Tiers grade effort, not feasibility.
| Tier | Meaning | Default status |
|---|---|---|
direct |
Deterministic 1:1 mapping | Active |
normalize |
Mapped, but a parameter needs a human eye | Draft |
manual |
Custom SQL. The expression must be authored by hand | Draft |
Nothing is skipped. An unrecognized generic test or a singular SQL test still produces a check, as a satisfiesExpression in Draft with the dbt source recorded in its metadata, so a reviewer adapts it in Qualytics rather than going back to the dbt project to find it.
Status is yours to set
The tier-to-status mapping above is the default, not a policy. --status overrides it wholesale:
# Review everything before anything fires
qualytics dbt import -m target/manifest.json --datastore-id 42 --status Draft
# Activate everything, including checks that need editing
qualytics dbt import -m target/manifest.json --datastore-id 42 --status Active
The default exists because manual checks carry an empty expression, and normalize checks carry an assumption a reviewer should confirm. For example, volumetric checks assume a single-day window because dbt has no equivalent concept. Those checks may not evaluate meaningfully until reviewed. --status Active prints how many fall into that group and then does what you asked.
Tests that become two checks
A few dbt tests assert two things at once, and split:
| dbt test | Becomes |
|---|---|
expect_column_value_lengths_to_be_between |
minLength + maxLength |
expect_column_value_lengths_to_equal |
minLength + maxLength (same value) |
Each half gets its own check, so both are created and updated independently. A one-sided dbt test emits only the half it specifies: min_value alone produces just a minLength. Because of this, plan reports the check count and the dbt test count separately when they differ.
What carries across
where becomes filter. A dbt test scoped with config.where produces a check with the same SQL predicate in filter, which every Qualytics rule type supports. Dropping it would run the check against exactly the rows dbt was told to exclude.
Everything else lands in metadata. dbt facts with no direct Qualytics field are recorded on the check rather than discarded, so a reviewer completing a Draft never has to reopen the dbt project:
filter: status != 'deleted' # from config.where
additional_metadata:
_qualytics_check_uid: dbt__test_jaffle_not_null_proportion_stg_orders_amount_abc
dbt_unique_id: test.jaffle.not_null_proportion_stg_orders_amount.abc
dbt_test: dbt_utils.not_null_proportion
dbt_package: jaffle
dbt_severity: warn # only when it differs from dbt's default
dbt_tags: nightly
dbt_limit: 500
dbt_kwargs: '{"at_least": 0.95}' # every argument the mapping did not consume
dbt_compiled_sql: ... # singular tests
dbt_kwargs is the important one: when a rule maps but its parameters don't, the original thresholds stay visible on the check itself. Tags arrive as a comma-separated string and dbt_kwargs as a JSON-encoded string, so parse the latter before processing its values.
Idempotency
Each check's _qualytics_check_uid is derived from the dbt test's unique_id, so re-running import after the dbt suite changes updates checks in place rather than duplicating them. This also means several dbt tests on the same container, rule, and field (say, two expression_is_true tests on one column) stay separate checks instead of overwriting each other.
Status on re-import
By default import sets the status from the tier, which means a re-import resets a check someone activated in the platform back to Draft. Two ways to handle it:
- Config-as-code: treat the YAML written by
--emit-yamlas the source of truth. Activate by editingstatusin the emitted file, not in the web app. See Config as Code. --preserve-status: omit status entirely so the importer keeps whatever each check currently has.
Supported dbt tests
Native dbt, dbt_utils, and dbt_expectations generic tests are mapped. Highlights:
| dbt test | Qualytics rule | Tier |
|---|---|---|
not_null |
notNull |
direct |
unique |
unique |
direct |
accepted_values |
expectedValues |
direct |
relationships |
existsIn |
direct |
dbt_utils.unique_combination_of_columns |
unique |
direct |
dbt_utils.accepted_range |
between |
direct |
dbt_utils.expression_is_true |
satisfiesExpression |
normalize |
dbt_utils.relationships_where |
existsIn |
normalize |
dbt_expectations.expect_column_values_to_match_regex |
matchesPattern |
direct |
dbt_expectations.expect_table_row_count_to_be_between |
volumetric |
normalize |
dbt_expectations.expect_row_values_to_have_recent_data |
freshness |
direct |
dbt_expectations.expect_column_value_lengths_to_be_between |
minLength + maxLength |
direct |
| Singular (bespoke SQL) tests | satisfiesExpression |
manual |
| Anything unrecognized | satisfiesExpression |
manual |
Run qualytics dbt plan --show-checks against your own manifest for the complete per-test mapping.
dbt_utils.relationships_where deserves a note: its to_condition becomes the reference filter on the referenced container, its from_condition becomes the check's filter (combined with config.where when both are set), and dbt-utils' no-op 1=1 defaults are dropped rather than carried over. It lands as normalize so a reviewer can confirm both conditions are valid in the target datastore before activation.
Where to look next
- Quality Checks: managing the checks the import creates
- Operations: sync (required before import), profile, and scan
- Config as Code: keeping exported checks in Git
- GitHub Actions pipelines: full CI patterns for the CLI