Skip to content

Check Sheet Migration

Create quality checks, and the computed tables or joins they depend on, from a single spreadsheet. The check sheet is a normalized tabular template (Excel or CSV, one row per check or computed container) designed to be filled from a client's own check catalog, so a migration that starts life in a tracking spreadsheet ends as checks in Qualytics without a hand-written script.

When to use which bulk path

Use migrate when the checks live in an external catalog or spreadsheet and do not yet exist in any Qualytics instance. If they already exist in another instance, use the Quality Checks export and import flow. If they are dbt tests, use dbt Import. For whole-datastore configuration trees in Git, see Config as Code.

Commands

Command Description
migrate plan Validate and summarize a sheet (offline, no authentication)
migrate validate Everything plan checks, plus read-only resolution and SQL validation against target datastores
migrate apply Create the sheet's computed containers and checks on the target instance

Workflow

graph LR
    S[check sheet] -->|migrate plan| P[Offline summary and issues]
    S -->|migrate validate| V[Names, references, and SQL resolved against the target]
    S -->|migrate apply| Q[(Qualytics datastore)]
    Q --> A[Run artifacts: run.log, results.csv, yaml/]
  1. Fill the sheet from the source catalog.
  2. migrate plan catches structural problems without touching anything.
  3. migrate validate --datastore-id N proves every name, reference, and SQL query resolves on the target. Still creates nothing.
  4. migrate apply --datastore-id N creates computed containers first, waits for each one's profile, then imports the checks.

Every row problem is reported with its row number and check_id; rows with errors are skipped, never half-applied.

The check sheet

One .xlsx or .csv file. Column headers are matched case-insensitively with spaces and punctuation normalized, so Check ID, check_id, and CHECK-ID are the same column. A workbook with several tabs is read from the first tab by default; --worksheet picks another by name or position. Unrecognized columns are ignored with a single warning naming them.

Identity columns

Column Meaning
check_id Required. Your catalog's key for the row. Duplicates are errors. Stamped verbatim as additional_metadata.legacy_check_id, which is both the trace back to the source row and the identity re-applies update against.
kind Blank or check (default) for a quality check; computed_table or computed_join for a computed container the checks depend on.
datastore Optional per-row target (name or numeric ID). Rows without it go to every --datastore-id passed to apply; rows with it go only there.
container For checks, the target table or view name (matched by name, casing corrected against the synced schema). For computed rows, the new container's name.

Check columns

Column Meaning
rule_type The Qualytics rule, for example notNull, unique, freshness, existsIn, aggregationComparison, equalTo, between, matchesPattern, satisfiesExpression. Rules outside this list pass through with a warning; supply their properties via properties_json.
fields Comma or semicolon separated field names. Casing is corrected against the synced schema.
description Check description. Generated from check_id and the rule when blank.
filter Row-scope SQL predicate. Dropped with a warning on rules that reject filters, such as freshness.
coverage 0 to 1. Omitted on rules without coverage support.
tags Comma or semicolon separated tag names. apply --tag X appends X to every row. Tags are created on the target if missing.
status Active or Draft per row. The default is Draft, so everything is reviewed before it fires.
anomaly_message_field Field whose value becomes the anomaly message.

Rule properties

Column Used by Notes
value freshness (max age: raw milliseconds or shorthand like 45s, 90m, 36h, 7d, 2w), equalTo, greaterThan, lessThan (numeric, with inclusive)
min, max, inclusive_min, inclusive_max between One-sided ranges narrow to greaterThan or lessThan automatically; inclusivity defaults to true
inclusive equalTo, greaterThan, lessThan Defaults to true
pattern matchesPattern Regular expression
expression satisfiesExpression, aggregationComparison (left-side aggregate)
comparison aggregationComparison lt, lte, eq, gte, gt; also accepts <, <=, =, >=, > and long forms
ref_expression aggregationComparison Right-side aggregate, evaluated on the referenced container
ref_container existsIn, notExistsIn, aggregationComparison Referenced container name, resolved on the target at apply time
ref_field existsIn, notExistsIn The referenced field's name
ref_datastore Cross-datastore references Name or numeric ID. Omit when the referenced container lives in the same datastore
ref_filter existsIn, aggregationComparison SQL filter on the referenced container
properties_json Any rule JSON object merged over the flat columns, the escape hatch for anything not listed above. A key set by both a flat column and the JSON with different values is an error

Required columns are validated per rule before anything runs: freshness requires value; existsIn requires fields, ref_container, and ref_field; aggregationComparison requires expression, comparison, ref_container, and ref_expression.

Computed container columns

Column Meaning
query The container's SQL. For computed_table this runs on the source database in its native dialect. For computed_join it is the join query written against the source aliases.
sources computed_join only: the joined containers as orders=o; customers=c pairs (the alias defaults to the name), or a JSON list. Names resolve to containers on the target at apply time.
description Container description.

A check row that targets a computed container declared in the same sheet just names it in container; apply orders the phases. Row order never matters between checks and containers. It matters only among computed containers themselves: a computed_join may read a computed table from the same sheet only if that table's row comes first.

Qualify tables inside computed SQL

A computed table's query runs directly on the source database, where tables usually need a schema prefix even though synced container names do not: write tpch.ORDERS, not ORDERS. The datastore's schema is shown by qualytics datastores get --id N. migrate validate checks each query against the source and reports failures like Invalid object name before anything is created.

Custom metadata columns

A column headed metadata:<key> writes <key> into every produced check's additional_metadata, key taken verbatim, casing and punctuation preserved. An empty cell means the key does not apply to that row, so one sheet can carry metadata:X for some rows and metadata:Y for others:

check_id,rule_type,container,fields,metadata:Business Domain,metadata:SLA Tier
100,notNull,orders,order_id,Treasury,
200,unique,invoices,invoice_id,,gold

legacy_check_id is reserved (it is set from check_id); a metadata: column naming it is an error.

Plan

Validate and summarize the sheet offline. No authentication, no network.

qualytics migrate plan --sheet week1.xlsx
qualytics migrate plan --sheet week1.xlsx --worksheet "Week 1" --show-checks
Option Type Default Required Description
--sheet, -s TEXT - Yes Path to the check sheet (.xlsx or .csv)
--worksheet TEXT first tab No Workbook tab to read: name or 1-based position
--status TEXT Draft No Default landing status for rows without one
--tag TEXT - No Tag to attach to every check (repeat for multiple)
--show-checks FLAG false No List every row and the check it produces
--emit-yaml TEXT - No Also write the converted checks to this directory
--strict FLAG false No Exit non-zero when the sheet has error rows

Problems are graded: errors (a missing required column, a duplicate check_id, an unknown kind) stop those rows from ever applying, while warnings (an unrecognized column, date logic with no explicit time zone conversion) apply but deserve a look.

Validate

Fail early. Everything plan checks, plus read-only resolution against target datastores: containers exist (or the sheet creates them), field names match the synced schema, every ref_container, ref_datastore, and ref_field resolves (each printed with the container it resolved to), join sources exist, and each computed container's SQL is checked against the source without creating anything. Exits non-zero on any problem.

qualytics migrate validate --sheet week1.xlsx --datastore-id 42
Validating against datastore 42 (read-only)...
  row 6 (770): SQL for 'orders_recon' validated against the source
  row 4 (326): ref 'CUSTOMER' → container 169 in datastore 43
  row 5 (211): ref 'REGION' → container 176 in datastore 43
  datastore 42: all references resolve

Validation passed.
Option Type Default Required Description
--sheet, -s TEXT - Yes Path to the check sheet (.xlsx or .csv)
--worksheet TEXT first tab No Workbook tab to read: name or 1-based position
--datastore-id INTEGER - No Target datastore to resolve against (repeat for multiple); omit for offline-only checks
--validate-sql / --no-validate-sql FLAG --validate-sql No Check each computed container's SQL against the source

If the instance itself cannot be reached (an expired sign-in, a connectivity problem), validate reports that once as an instance problem rather than flagging every computed row, so a real SQL mistake is never confused with an outage.

Apply

Create everything on the target. Two phases, in order:

  1. Computed containers. Every specification is validated first; a validation failure stops the phase before anything is created. Containers are then created in declaration order, and the CLI waits for each container's own profile operation to finish before moving on, so dependent joins and checks always see profiled fields.
  2. Checks. Container and field names are case-corrected against the synced schema, cross-references resolve to the right datastore, and checks are created or updated in place. Failures are reported per row and never stop the run.
# Preview: no changes, dependent checks count as creates
qualytics migrate apply --sheet week1.xlsx --datastore-id 42 --dry-run

# Apply
qualytics migrate apply --sheet week1.xlsx --datastore-id 42

# Re-apply after sheet edits, keeping checks a reviewer already activated
qualytics migrate apply --sheet week1.xlsx --datastore-id 42 --preserve-status
Option Type Default Required Description
--sheet, -s TEXT - Yes Path to the check sheet (.xlsx or .csv)
--worksheet TEXT first tab No Workbook tab to read: name or 1-based position
--datastore-id INTEGER - No Target datastore for rows without a datastore column (repeat for multiple)
--dry-run FLAG false No Preview what would be created or updated
--status TEXT Draft No Default landing status for rows without one
--preserve-status FLAG false No Omit status so re-applies keep what is set in the platform
--tag TEXT - No Tag to attach to every check (repeat for multiple)
--validate-fields / --no-validate-fields FLAG --validate-fields No Check field names against the synced schema and correct their casing
--skip-containers FLAG false No Skip the computed-container phase (containers already ensured)
--on-existing TEXT skip No What to do when a computed container already exists: skip or update
--force-drop-fields FLAG false No With --on-existing update: allow definition changes that drop fields carrying checks (the platform preserves those checks; they reactivate if the fields reappear)
--profile-timeout INTEGER 900 No Seconds to wait for each created container's profile operation
--emit-yaml TEXT - No Also write the converted checks to this directory
--run-dir TEXT migrate-runs/<timestamp> No Directory for this run's artifacts; empty string disables
--results-csv TEXT <run-dir>/results.csv No Per-check results ledger path; empty string skips
--failures-log TEXT <run-dir>/failures.log No Failure log path
--strict FLAG false No Exit non-zero when the sheet has error rows or anything fails

Updating existing computed containers

--on-existing update compares the sheet against the live definition and only sends real changes: an identical definition is reported unchanged, a description or metadata edit takes a light path that never re-profiles, and a genuine SQL change triggers validation, the update, and a fresh profile. A change that would drop fields carrying quality checks is refused by the platform; add --force-drop-fields to proceed (the affected checks are preserved and reactivate if the fields reappear), and update the sheet's dependent check rows to the new field names.

Run artifacts

Every real run leaves an audit folder (dry runs write nothing):

migrate-runs/20260917-150505/
├── run.log        # the full terminal transcript, timestamped per line
├── results.csv    # one row per check: check_id, created/updated/failed,
│                  # container, rule, status, a link to the check, and the
│                  # failure reason for failed rows
├── failures.log   # failure detail
└── yaml/          # the definitions exactly as applied, per datastore

results.csv is the receipt that goes back to whoever owns the source catalog: their ID, what happened to it, and where it now lives.

Cross-datastore references

ref_container never resolves globally. With ref_datastore filled, the name is looked up only in that datastore; without it, only in the row's own target datastore. A name that does not resolve in the scoped datastore is an error, never a guess, and names whose casing is ambiguous on the target (say Orders and ORDERS both exist) are rejected with both variants named.

check_id,rule_type,container,fields,ref_datastore,ref_container,ref_field
900,existsIn,STG_POSITIONS,PORTFOLIO_ID,Analytics Warehouse,portfolio_status,PORTFOLIO_ID

This row's check lives on STG_POSITIONS in the apply target, and its reference resolves inside the Analytics Warehouse datastore. migrate validate prints the resolved container for every reference so a wrong scope is visible before anything is created.

Idempotency

Checks are matched by legacy_check_id, the sheet's own check_id. Re-running apply after sheet edits updates checks in place rather than duplicating them, and two same-shaped checks with different IDs stay separate. Because a re-apply sets status from the sheet by default, use --preserve-status so checks a reviewer activated in the platform stay active.

Where to look next