Skip to main content
This page is the published copy of the operating guide that ships with the tool as AGENTS.md — in the release archive, in getsynq/synq-recon, and at /opt/synq-recon/AGENTS.md inside the Docker image. It is deliberately denser than the rest of this section. Start at Getting started if you want the guided version, and see the CLI reference for every flag.
This guide tells a coding agent how to use synq-recon on a reconciliation suite. Read it from the top down. A reconciliation compares one dataset in a source database with one dataset in a target database. A suite is a YAML file with several reconciliations and their connections. By default, only counts and checksums leave the database, never row values.

1. The loop

Do these steps in order. Do not skip a step, because each step costs more than the step before it.

Step 1: validate offline with check-config suite.yaml

The command checks the YAML and warns about time-dependent SQL. It connects to no database. Continue when it prints Configuration check passed!. A warning about NOW() or CURRENT_DATE is real, see Never do these.

Step 2: read the plan with plan suite.yaml

The command prints the exact SQL of each side after variables, table references and time-travel snapshots resolve. It connects to no database. Continue when the SQL is the SQL you meant. This is the cheapest place to find a wrong date or a wrong schema. Values that a run derives from live data, such as a cutoff watermark, are named but not resolved.

Step 3: validate against the databases with check-config suite.yaml --db

The command sends each query to the query planner (LIMIT 0). It reports the columns, the table size, the keys and a suggested key column, and warns when your key column has no index. Continue when every connection is OK and every query validates. --db does not run the setup: SQL. If setup creates the tables, every query fails here, so go to step 4.

Step 4: compare with run-check suite.yaml

The command compares each reconciliation without a drill, with one query per side. If it reports MATCH, the datasets agree and you are done. If it reports MISMATCH, go to step 5. Exit code 1 is a result, not a failure. Exit code 2 means that a reconciliation could not run, so read its error first.

Step 5: locate with run suite.yaml --auto-drill

The command runs the check, then bisects the key space of each mismatch to find the key ranges that differ. run-drill does the same without the check. Continue when the drill reports usable key ranges. If it reports thousands of leaves, stop and read Never do these.

Step 6: save to the workspace with upload-config suite.yaml

Run auth whoami first. The command stores a version of the suite in your Coalesce Quality workspace, under Development, and prints the suite id.

Step 7: run on the backend with run-remote <suite-id> --wait

Coalesce Quality runs the suite against the workspace integrations. Connections bind to integrations by name, or explicitly with --map connection=integration_id. Pass --drill=false if you do not want a drill.

Step 8: publish to production with promote <suite-id>

The command creates a deployment: a snapshot of the suite and an optional schedule. Its results become assets, checks and issues. If you edit the suite, production does not change until you promote again. Run promote --diff first, see Editing a deployment.

After a run

recheck <run> runs the same queries again and reports what changed. drill-deeper <run> continues the drill, see Investigating a finished run.

2. Never do these

  • Do not query the warehouse by hand to “check something first”. Write the question as a reconciliation, so that the result is recorded and reproducible. If the tool cannot express it, report a bug.
  • Do not commit .connections.yaml or any credential. Keep the real file in .gitignore. Commit only a .connections.yaml.example with ${ENV_VAR} placeholders. Do not upload-config a suite that holds a literal secret.
  • Do not run a drill with threshold 1 on a table with tens of thousands of real differences. Bisection is O(log n) only when most rows are identical. When most rows differ, it sends thousands of queries that each scan a large part of the table. First find the groups that differ with an aggregate comparison (mode: aggregate, a COUNT measure grouped by the natural partition). Then drill a query that reads only those groups.
  • Do not omit --drill on run-remote if you want no drill. An ad hoc run has no deployment to inherit from, so it drills every mismatch whose reconciliation has bisection.enabled: true, the default. Pass --drill=false. Only run-drill and drill-deeper drill a reconciliation with bisection.enabled: false.
  • Do not put NOW(), CURRENT_DATE or GETDATE() in a query. The two sides evaluate them at different instants, which makes false differences. Use a template variable such as {{ today - 7d }}, which resolves once for both sides. strict_time_references: true makes the check-config warning an error.
  • Do not make a concatenated key such as a || '|' || b. It cannot use an index, so each segment scans the full table. Write key_columns: [workspace, path], and bisection filters on the column tuple.
  • Do not compare a replica that lags without a cutoff:, see Avoiding false differences.

3. Confirm the target before any mutation

Each command that writes to a workspace acts on the workspace of your credential: upload-config, promote, unpromote, trigger, run-remote, deployment …, suite delete and runs cancel.
Before the mutation, read the workspace: line and confirm it is the correct workspace. There is no --workspace flag. To change the target, change the credential.

Credential order

The CLI uses the first credential it finds, so an automated pipeline never uses the browser login of a developer:
  1. Client credentials: --client-id / --client-secret, or QUALITY_CLIENT_ID / QUALITY_CLIENT_SECRET, or the synq: block of the suite.
  2. An API token: QUALITY_TOKEN.
  3. The browser login from synq-recon auth login. The CLI refreshes it automatically.
To log in with a Coalesce account, see synq-recon auth login --help. That credential carries your Coalesce Quality roles, not the permissions below. The --synq- flags and the SYNQ_ environment variables are older spellings and still work.

Region and endpoint

--region eu|us|au and --endpoint (or QUALITY_API_ENDPOINT, for staging or self-hosted) are root flags. They also select the stored credential of that region, and a token never goes to another region. Usually you need neither: auth login --region au records the deployment for later commands, and auth use <region> or auth use --clear changes it. The order is --endpoint, --region, the synq.endpoint of the suite, QUALITY_API_ENDPOINT, QUALITY_REGION, the last login, then eu.

Permissions

SCOPE_RECON_READ lets you inspect. SCOPE_RECON_EDIT lets you upload-config, edit suites and report the results of a local run. SCOPE_RECON_PROMOTE lets you promote and trigger. A workspace API client can have all three, so every command works without a person.

4. Authoring a suite

The smallest useful suite is a connection pair and one reconciliation:
Authoring suites and Comparison modes explain each part. The Configuration reference lists every field, type and default. For completion and validation in your editor, point it at the schema:

Rules that decide if a suite works

  • Use table: when you can. The tool then finds the columns, and you can narrow with columns:, exclude_columns: and where:. Use query: only when the comparison needs SQL. A query: takes columns: and exclude_columns: too. A cutoff filters the query as written, so its column need not be in columns:. where: is for a table: only. A suite with where: beside a query: does not load, so put the filter in the query.
  • exclude_columns: matches a name in any case, quoted or bare. The run warns about an entry that matches no column, because it excludes nothing.
  • Key columns must be unique and should have an index, because bisection sorts and filters on them. Keep them in the columns: of each side and out of exclude_columns:. If a side leaves a key column out, the drill fails at the first difference.
  • Each mode except row_count needs the same number of columns on both sides, because the checksum hashes the columns by position. A wider target fails with source has N columns but target has M columns. Narrow both sides with columns:, or remove the extra columns with exclude_columns:. row_count reads no columns, so it needs only the key columns on both sides.
  • Names that differ only in case match automatically. For other names, use column_mapping.
  • Write a name as you write it unquoted, and quote only to pin it. The tool quotes each part of table: and each entry of columns: for its warehouse, so a reserved word such as order works. An unquoted name resolves as the warehouse resolves it: orders is ORDERS on Snowflake and orders on Postgres. For a name created with quotes, quote that part: table: 'DB.SCH."events"'. The outer single quotes are YAML syntax. key_columns, column_mapping and cutoff: match column names in any case.
  • Keep credentials in .connections.yaml, keyed by the connection names. The CLI finds it automatically, or you pass --connections. In the workspace, the same names bind to integrations, so one suite runs in both places.
  • Do not set motherduck_account, the CLI sets it. Write the MotherDuck account name in database:, and pass motherduck_token.

Finding the physical table path

If the warehouse is an integration of your workspace, the catalog finds the physical database.schema.table of a side:
Both print the database, schema and table, and the dialect and connection id, to choose between warehouses with the same name. The catalog is optional. A DuckDB file or a local container is not in it, and no other command reads it.

Avoiding false differences

Most “the data is wrong” findings are a problem with the comparison window. Try these first. Time windows and cutoffs explains cutoff:, window: and variables:.

Numeric and timestamp precision

Comparison modes gives the defaults, the precedence and examples. The traps:
  • numeric_scale: <places> rounds half away from zero. It is not a tolerance: 10.1249 and 10.1250 round to different values at two places.
  • numeric_scale: exact fails if the two sides declare different scales. On a floating-point column, it works only between two databases of the same kind.
  • comparison: applies to row_checksum only. Other modes ignore it with a warning.
  • check-config --db reports every column that exact cannot compare. A run stops at the first one.

5. Reading the output

Output formats

Set the format explicitly. -o is table in a terminal and toon when the CLI finds an AI agent environment. To parse output, pass -o json. run, run-check and run-drill have only two formats: -o json and a text report. --jq, --columns, --no-headers and --wide have no effect on them, so send their -o json to jq. The workspace commands accept every format and these four flags. run --auto-drill -o json is not one JSON document. It prints the check document, a text banner, then one drill document for each reconciliation. To parse the results, run run-check -o json and run-drill -o json separately. Each prints one document. The results of an aggregate reconciliation are a separate array, so --include one mode at a time.
Logs go to stderr and results go to stdout. When you pipe -o json, use 2>/dev/null, never 2>&1.

Exit codes

Each code has the same meaning in every command: A local run with a difference and a failed reconciliation exits 2. Thus 1 always means that every reconciliation has a result. A remote run that completes can still report a mismatch, which is normal. --fail-on takes several statuses, separated by commas or with the flag repeated (--fail-on mismatched,within_threshold). The list replaces the default. --fail-on=passed makes a check for data that must differ.

Audit logs

--audit-log <path> writes the full record of a run as JSON: every query, timing, count, checksum and mismatch leaf. For a file name made by the CLI, give a directory with a trailing /. The format is the AuditLog schema, rendered or as audit-log.schema.json.
Each mismatch leaf has a drill_stop_reason, diff_queries with SQL for both sides, and all the mismatch_types that apply: Do not classify a difference by the count only. A target with one more row can still miss a row and hold two orphans. Read the types. Two traps in the audit log:
  • The local --audit-log file uses snake_case (bisection_result). The same log from audit-logs get <invocation-id> -o json uses camelCase (bisectionResult). A jq path for one returns null on the other.
  • Counts and checksums are 64-bit integers, which the audit log writes as strings ("source_count": "10"). run-check -o json writes numbers. Use tonumber to compare.

6. What each command costs

On a warehouse that charges for use, each query of a run costs money. Know the cost before you put a command in a loop.

Cost traps

  • The table analysis of check-config --db can grow with the warehouse, not with the suite. On BigQuery, and other platforms without a metadata API per table, it reads the metadata of whole datasets. On a large warehouse this is slow and costs money. Thus only check-config --db runs it.
  • --max-table-rows and --max-table-bytes make run, run-check and run-drill estimate each scan first, with a dry run or EXPLAIN. The CLI warns when an estimate is above the limit. Both are 0 (off) by default. BigQuery and Snowflake estimate bytes. ClickHouse and PostgreSQL estimate rows. DuckDB, Athena and Databricks give no estimate, so no warning occurs.

Limiting the load of a local run

  • --concurrency sets how many reconciliations run at the same time. Each reconciliation queries both sides, and a drill can query many segments at the same time.
  • connections.<name>.parallelism sets the maximum number of queries a local run sends to that connection at the same time. The default is 8.
  • connections.<name>.disabled: true stops a local run from running a reconciliation, setup or teardown that uses that connection. If --include names such a reconciliation, the command fails.
These connection settings apply only to local runs. A suite in the workspace uses integrations.

Keeping a local run local

If a stored credential exists, a local run reports its results to Coalesce Quality, even when all its databases are local. It logs the endpoint at INFO, and prints a link to the stored run to stderr. A reported run never sends row values. To send nothing and resolve no credential, pass --no-report or set RECON_NO_REPORT. reporting.level: detailed is for local runs only. Coalesce Quality does not save, promote or run a suite with detailed. In a workspace suite, use with_keys or count_only. To keep detailed and local fixtures in the same file, put them in a local: block, see Settings for local runs only. upload-config and Upload config in the app drop the local: block, so the workspace never runs your fixtures.

7. Investigating a finished run

To get the SQL queries of a finished reconciliation run back, or to replay the run, use audit-logs queries, recheck and drill-deeper. They take a finished run, not a suite: a local audit log file or the invocation id of a stored run.

Getting the SQL back

audit-logs queries <run> prints the investigation queries of a finished run: a stored run, a backend run, or a run whose output you lost. It connects to no warehouse.
A bisection drill gives one query pair for each range of adjacent mismatched segments. --all-leaves also prints the query of each segment. An aggregate comparison gives one pair for each group that differs. A run that matched, or did not drill, has no queries.

Replaying a run

A replay sends the queries of the original run, with the same variables and cutoffs, so it compares the same window. To use an edited query or the current data, pass --reresolve. An as_of snapshot needs --reresolve, or it can never pass. An audit log holds connection names, never credentials, so pass --connections. --remote sends the replay to the backend, and needs the invocation id of a stored run. Re-check and Drill deeper gives the other rules.

8. Command reference

synq-recon <command> --help is the full reference, and the CLI reference page has the same content. This section is the map.

Editing a deployment

promote on an existing deployment, and deployment update, change only the settings you pass. Thus a new snapshot never removes the schedule or the API trigger. To remove a schedule, pass deployment update --clear or promote --clear-schedule. A first promote uses the defaults: no schedule, no API trigger, and drill on. Run promote <suite-id> --diff before each promote. A promote replaces what production runs, and the only undo is another promote:
It prints a YAML diff of the deployed snapshot against the stored suite. Then it labels each deployment setting: changed, unchanged, preserved (flag not passed), or the default of a first promote. Without these labels, an omitted --schedule and a removed schedule look the same. The deployment id is part of the asset path of each reconciliation. A new deployment with a new id moves its checks and drops their history. A promote of the same suite uses the same id. To set the id when you rebuild a deployment, use promote --deployment-id <id>.

Check categories

Set a check category on the deployment, not in the suite file, because a reconciliation is a check only after a promote. --category is the technical category and --governance-category is what the check is for, see Check categories.
An omitted flag keeps the stored value. An empty value (--category "") removes the declaration. A case overrides only the categories you name. Without a declaration, a quality.check_category / quality.governance_category annotation decides, else the categorisation rules of the workspace.

Flags with traps

  • --timeout is the time limit of a local run. The default is the execution.timeout of the suite, else 5m. The time limit of a backend run is --execution-timeout, which falls back to the deployment, then to execution.timeout. For one reconciliation, set execution.reconciliation_timeout on the suite or timeout on the reconciliation. Such a reconciliation fails with a timeout, and the run continues.
  • --concurrency falls back to execution.concurrency, else 1. Backend runs read execution.concurrency and can use a lower limit.
  • --connections finds .connections.yaml or connections.yaml in the working directory automatically. --dbt-profiles reads connections from a dbt profiles.yml if the suite does not define them.
  • --force skips safety checks too, not only confirmation prompts.

9. Worked example

This example uses the DuckDB fixtures in examples/ of a release archive. It needs no warehouse, credentials or network. The suite has two differences on purpose: one missing order and one wrong amount.
  1. Validate:
  2. Compare:
    The exit code is 1: differences found, as expected.
  3. Locate the differences and keep the audit log:
    Order 1005 is missing on the target. Order 1010 is on both sides, but its checksum differs, so a value changed. Only two of ten rows needed a look.
  4. Get the SQL that shows the rows, and run it on the two connections:
  5. Check again after a fix. The suite defines its connection, so a replay needs a connections file with that name:
    Nothing changed because nothing was fixed.
  6. Try the aggregate mode, which finds a difference by group, not by key range:
examples/ has one scenario for each business case. ./examples/test-all-examples.sh runs all of them.

10. When something goes wrong