YAML is the primary surface. The app’s wizard is a fast path to a common shape, and the app can also take a suite as pasted YAML — but everything the product can do is expressible in the file, and some things are only expressible there. What needs YAML is the honest map.
Anatomy of a suite
suite.yaml
Configuration reference
Every field, rendered and browsable.
config.schema.json
The machine-readable schema, for editors and validators.
The smallest useful suite
A connection pair and one reconciliation:suite.yaml
Choosing the dataset
Each side of a reconciliation is a dataset: a connection plus what to read.- table (preferred)
- query
table: the tool resolves the column list for you, which means it can compare columns you did not have to enumerate, warn when the two sides disagree on shape, and narrow the comparison without you writing SQL:columns and exclude_columns are mutually exclusive. The table reference accepts a dotted string (db.schema.table) or a structured object with database, schema and name.Key columns decide everything
key_columns: (or key_column: — both accept a string or a list) names the column tuple that identifies a row. It is the single most consequential choice in a suite, because the drill-down orders and range-filters on it.
Two rules:
- It must be unique. A non-unique key makes segment boundaries ambiguous and the drill-down unreliable.
- It should be indexed. Each drill-down level range-filters on the key. With a matching primary key or index, the database prunes; without one, every segment is a full scan.
check-config --dbtells you which you have.
key_column; several as a key_columns list. That is only a formatting detail of synq-recon suite yaml output — write whichever you prefer.
Matching columns across the two sides
Column names that differ only in case match automatically, souser_id on Postgres lines up with USER_ID on Snowflake with no configuration. For genuinely different names, map them:
case_insensitive: false to turn the automatic behaviour off entirely.
Keep credentials out of the suite
Move the wholeconnections: block into a separate file, git-ignored, keyed by the same connection names:
.connections.yaml
.connections.yaml or connections.yaml in the working directory; --connections <path> points at one explicitly. Commit a .connections.yaml.example with ${ENV_VAR} placeholders so the next person knows which names to fill in.
Anywhere in a suite, ${VAR} reads an environment variable and ${VAR:-default} supplies a fallback.
Setup and teardown
SQL to run before and after the comparisons, per connection. Useful for staging a snapshot, materialising a view, or building fixtures in a test suite:setup_file and teardown_file take a path per connection instead of inline statements. Both blocks also exist per reconciliation, where teardown_on_failure and ignore_setup_errors inherit the suite’s value unless you set them.
ignore_setup_errors: true is right for idempotent bootstrap SQL (CREATE TABLE IF NOT EXISTS) and wrong when the setup is what produces the data being compared — you would be comparing two empty tables and calling it a match.Annotations
Name/value labels on a suite or a single reconciliation. Reconciliation-level annotations merge with the suite’s, and they follow a promoted suite into the platform, where they annotate the resulting assets and checks.Run timeout and parallelism
How a run of the suite executes, stored with the suite so a local run, an ad-hoc run in the workspace and a production deployment all read the same values:timeout a local run stops after 5 minutes and a run in the workspace after 15. Without concurrency the reconciliations run one after another, so one slow table holds up the rest.
A run that hits its timeout is cancelled. Reconciliations that finished keep their results, the ones still running are reported as failed.
Without reconciliation_timeout, a single slow query, like a cutoff watermark over an unindexed column, can use up the run’s whole timeout, and every reconciliation after it then fails without running. With it, a reconciliation that runs past its budget (setup, check and any drill together) is reported as failed with a timeout, and the run moves on to the next one. Set timeout on a reconciliation to give that one a different budget:
error_handling.query_timeout. It covers the reconciliation’s setup and teardown statements too. A query that hits it fails with a timeout and is not retried, because a query that took that long usually takes that long again.
If the query that times out is a cutoff watermark, point cutoff.query at something cheaper to read than the full table, like a load-metadata table or the most recent partition.
Teardown still runs after the run’s timeout expires, as long as teardown_on_failure asks for it.
Each reconciliation running in parallel queries both warehouses at once, so concurrency multiplies the load on them. Every connection still caps its own parallel queries with parallelism: (default 8).
A more specific setting wins over the suite’s:
Runs in the workspace clamp the timeout to a supported range and may cap concurrency lower than the suite asks for.
reconciliation_timeout has no per-run override: every run reads it from the suite. In the app, all three settings are on the suite’s Execution tab, and a reconciliation’s own timeout is on its Overview tab.
Validate before you spend a query
Three steps, cheapest first. Do not skip ahead; each one catches a class of problem the next would surface as a confusing mid-run error.1
Parse and sanity-check — costs nothing
Configuration check passed!.2
Check the wiring
connection: values are workspace integration ids rather than local definitions, offline validation reports no connections defined — this is the step that tells you why.3
Check against the databases
LIMIT 0 — no scan, no rows. It reports the resolved columns, table size, primary and partition keys, a suggested key column, and a warning when the key column you chose is not indexed.Move on when every connection is OK and every query validates. A failure here is a wrong table name, a missing grant, or a column that does not exist.On BigQuery, the table analysis in
--db enumerates dataset metadata rather than jumping to the tables you named, so its cost scales with the size of the warehouse rather than the size of your suite. That is why the analysis is confined to this interactive command and does not run before every comparison.What the wizard covers and what needs YAML
In the app, Health → Reconciliations → Development has a new-suite wizard with four steps — Data Sources, Comparison, Suite Info, Review — and an Upload Suite Config dialog that takes a suite as pasted or uploaded YAML. The wizard is quicker for a first suite and better at discovery: it searches your catalog, so you can pick a table you already have rather than typing warehouse coordinates, and it shows which columns are keys or indexed while you choose. It covers a deliberate subset of the format:
The same forms edit a stored reconciliation. Anything they cannot edit is listed under Configured in YAML when you open it, and saving the form keeps those settings exactly as they are.
Two capabilities exist only in the app, and are worth knowing about:
- Authoring from the catalog. The entity picker resolves a table you have already catalogued to its warehouse coordinates. From the CLI you supply coordinates yourself.
- Execution-plan preview. The app renders the resolved plan for a stored suite before you run it.
A complete example
Two reconciliations over the same pair of systems, in two different modes — row-level checksums for orders, and grouped totals for inventory, where row-level comparison would be the wrong instrument:ecommerce.yaml
Next
Comparison modes
Which mode to use, aggregate measures, and threshold resolution.
Time windows and cutoffs
Comparing two live systems without inventing differences.
Running locally
Filters, concurrency, JSON output, audit logs and exit codes.
Workspace suites
Save it, run it on our backend, promote it to production.