This page is the published copy of
AGENTS.md, the operating guide that ships
with the tool — in the release archive, and at /opt/synq-recon/AGENTS.md
inside the Docker image. It is written to be read top-down and acted on, so 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.1. The loop
Work through these in order. Each step has a condition for moving on; do not skip ahead, because every later step is more expensive than the one before it. 1. Validate offline —check-config suite.yaml
Parses the YAML, checks required fields, and warns about time-dependent SQL.
Costs nothing, touches no database.
Move on when it prints2. Validate against the databases —Configuration check passed!. Fix errors here rather than discovering them mid-run. Warnings aboutNOW()/CURRENT_DATEare real — see Never do these.
check-config suite.yaml --db
Connects to every connection, runs each query through the database’s query
planner (LIMIT 0), and reports the resolved columns. It also reports table
size, primary/partition keys, a suggested key column, and warns when the key
column you chose is not indexed.
Move on when every connection is3. Compare —OKand every query validates. A failure here is a wrong table name, a missing grant, or a column that does not exist — all of which would otherwise surface as a confusing mid-run error.--dbdoes not run the suite’ssetup:SQL, so a suite whose tables are created bysetupwill report every query as failed. That is expected; skip to step 3 for such suites.
run-check suite.yaml
One query per side: total row count and checksum. Fast and cheap regardless of
table size.
If it reports4. Locate —MATCH, the datasets agree and you are done. If it reportsMISMATCH, go to step 4. Exit code 1 means “differences found”, which is a result, not a failure.
run suite.yaml --auto-drill
Runs the quick check, then bisects the key space on whatever mismatched to
narrow the difference to specific key ranges. run-drill does the same without
re-running the quick check.
Move on when the drill reports mismatch leaves with usable key ranges. If it reports thousands of leaves, stop and read Never do these — you are drilling something that should be localised with an aggregate comparison first.5. Save to the workspace —
upload-config suite.yaml
Stores and versions the suite in your Coalesce Quality workspace, where it
appears under Development. Requires SCOPE_RECON_EDIT.
Run auth whoami first. Move on when it prints the suite id.
6. Run it on the backend — run-remote <suite-id> --wait
Executes the suite in Coalesce Quality against workspace integrations rather
than against databases this machine can reach. Connections bind to integrations
by name; override with --map connection=integration_id.
Move on when the run completes.7. Publish to production —--waitexits0if everything passed,1if a--fail-oncondition was hit,2if execution itself failed. Pass--drill=falseexplicitly if you do not want a drill — omitting the flag does not mean “off”.
promote <suite-id>
Creates a deployment: an immutable snapshot of the suite plus an optional
schedule, so it runs on its own and its results become platform assets, checks
and issues. Requires SCOPE_RECON_PROMOTE.
A deployment is a snapshot, not a pointer: editing the suite afterwards changes nothing in production until you re-promote. Re-promoting preserves settings you omit; a fresh promote applies defaults, and the drill default on a fresh promote is on.Any time after a run:
recheck <run> re-executes what it ran and reports
what moved; drill-deeper <run> continues its drill from where it stopped. Both
take a local audit-log file or the invocation id of a stored run. See
Investigating a finished run.
2. Never do these
Each of these has already cost someone real time. Do not hand-query the warehouse to “check something first.” Express the question as a reconciliation and letsynq-recon run it — that way the result
is recorded, reproducible, and comparable to the next run. If the tool cannot
express what you need, that is a bug worth reporting, not a reason to open a SQL
client.
Do not commit .connections.yaml, or any credential, ever. Keep the real
file git-ignored and commit only a .connections.yaml.example whose values are
${ENV_VAR} placeholders. A suite YAML with a literal secret in it is also a
suite you cannot safely upload-config.
Do not run a threshold-1 drill over a table with tens of thousands of genuine
differences. Bisection is O(log n) when the data is mostly identical; when
most rows differ it cannot halve, and it degenerates into thousands of queries
that each rescan a large slice of the table. Localise first: an aggregate
comparison (mode: aggregate, a COUNT measure grouped by the natural
partition) tells you which groups diverge in roughly one query per side. Then
drill a query narrowed to those groups.
Do not omit --drill on run-remote and expect no drill. Unlike promote,
where an omitted flag preserves the stored setting, an ad-hoc run has no
deployment to inherit from, so an omitted --drill falls through to each
reconciliation’s own bisection.enabled in the YAML. Pass --drill=false.
Do not put NOW(), CURRENT_DATE or GETDATE() in a query. They evaluate
at slightly different instants on the two sides, which manufactures differences
that are not there. Use a template variable, resolved once and interpolated into
both sides:
check-config warns about this; strict_time_references: true makes it an
error.
Do not synthesise a concatenated key. For a multi-column natural key, declare
key_columns: [workspace, path]. Bisection then orders and range-filters on the
column tuple, so a matching primary key or index still prunes each segment. A
synthetic a || '|' || b key cannot use the index and forces a full scan per
segment.
Do not compare a lagging replica without a cutoff. If the target trails the
source, rows still in flight look like missing rows. A cutoff: derives a
watermark from the data itself and filters both sides to at-or-below it. See
Cutoffs.
3. Confirm the target before any mutation
Every command that writes to a workspace —upload-config, promote,
unpromote, trigger, run-remote, deployment …, suite delete,
runs cancel — acts on whichever workspace your credential resolves to.
workspace: line and confirm it is the one you mean before the
mutation, not after. There is deliberately no --workspace override: your
credential determines the target, so the way to change it is to change the
credential.
Credentials resolve in a fixed order, so an automated pipeline never silently
picks up a developer’s browser login:
- Client credentials —
--client-id/--client-secret, orQUALITY_CLIENT_ID/QUALITY_CLIENT_SECRET, or thesynq:block in the suite. - An API token —
QUALITY_TOKEN. - The browser login cached by
synq-recon auth login, refreshed automatically.
--region eu|us|au selects the deployment, and --endpoint (or
QUALITY_API_ENDPOINT) points at a staging or self-hosted one. Both are root
flags, so they work on every command. Whichever you use also selects which
region’s stored credential is used — a token minted for one region is never
sent to another.
You rarely need either: synq-recon auth login --region au records the
deployment, and later commands resolve to it. synq-recon auth use <region>
switches between deployments you are already logged in to, and
auth use --clear goes back to the default (eu). Resolution order is
--endpoint > --region > the suite’s synq.endpoint > QUALITY_API_ENDPOINT
TheQUALITY_REGION> the last login >eu.
--synq--prefixed flags and SYNQ_-prefixed variables are the previous
spellings and still work.
Permissions: SCOPE_RECON_READ to inspect, SCOPE_RECON_EDIT for
upload-config, suite edits and reporting a locally executed run’s results,
SCOPE_RECON_PROMOTE to promote or trigger. All three can be granted to a
workspace API client, so every command works unattended.
4. Authoring a suite
The smallest useful suite is a connection pair and one reconciliation:table:beatsquery:when you can use it. Withtable:the tool resolves the columns for you and you can narrow withcolumns:,exclude_columns:andwhere:. Reach forquery:only when the comparison genuinely needs SQL.- Key columns drive everything. Bisection orders and range-filters on them,
so they must be unique and, ideally, indexed.
check-config --dbsuggests one and warns if yours is not indexed. - Modes.
row_countcompares counts only (fastest, misses modified values);row_checksumcompares counts and a checksum of every column (the default, catches any difference);aggregatecompares grouped measures likeSUMandCOUNTwith a tolerance, and drills down through agroup_columnshierarchy.fullis an accepted legacy spelling ofrow_checksum; writerow_checksum. - Column names differing only in case match automatically. For genuinely
different names use
column_mapping. - Keep credentials out of the suite. Put them in a git-ignored
.connections.yaml(auto-discovered, or passed with--connections) keyed by the same connection names. This is also what lets the same suite run locally and in the workspace, where the names bind to integrations instead.
- Configuration reference — every field, type, constraint and default, rendered.
config.schema.json— the machine-readable form. Point your editor at it for completion and validation:
motherduck_account appears under the
DuckDB connection because the loader populates it internally. Write database:
with the MotherDuck account name, and pass motherduck_token.
Avoiding false differences
Comparing two live systems is a moving target, and most “the data is wrong” findings are really a comparison window problem. Reach for these first.5. Reading the output
Pick your format explicitly.-o defaults to table for a human terminal
and to toon when an AI-agent environment marker is present — so as an agent
you will get toon unless you say otherwise. Anything parsing output must pass
-o json.
run, run-check and run-drill render only two shapes: -o json, and a
text report for every other value. --jq, --columns, --no-headers and
--wide do not apply to them; pipe their -o json through jq yourself. The
workspace commands (suite, deployment, runs, audit-logs, connections,
auth) honour all six formats and all four flags.
run --auto-drill -o json is not a single JSON document. It emits the
quick-check document, then a plain-text banner, then one drill document per
drilled reconciliation. To parse the results, run the two stages separately —
run-check -o json then run-drill -o json — each of which is one valid
document.
2>/dev/null, never
2>&1, when piping -o json.
Exit codes. Locally executed commands: 0 everything matched, 1
differences were found or the command failed. For run-remote --wait and
trigger --wait the codes are data-driven and finer: 0 passed, 1 a
--fail-on condition was met (default mismatched,failed), 2 the run itself
failed. A run can succeed and still report a mismatch — that is the normal case.
--fail-on=passed inverts the test for a “these should differ” canary.
Audit logs. --audit-log <path> writes the full record of a run — every
query, timing, count, checksum and mismatch leaf — as JSON. Pass a directory
(trailing /) for an auto-named file. Its shape is the published AuditLog
schema — rendered, or
audit-log.schema.json
to validate against.
Useful paths, using the local file’s casing:
drill_stop_reason saying why drilling stopped
there, diff_queries with ready-to-run SQL for both sides, and every
mismatch_types that applies — so a segment differing in both size and content
reports two:
Count direction alone does not classify a difference: a target with one row more
can still be missing a live row and carrying two orphans. Read the types.
Two traps in the same file:
Casing. The local--audit-logfile is snake_case (bisection_result,mismatch_leaves). The same log fetched from the workspace withaudit-logs get <invocation-id> -o jsonis camelCase (bisectionResult,mismatchLeaves). Ajqpath written for one source silently returnsnullagainst the other.
Counts are strings. Row counts and checksums are 64-bit integers, which JSON encodes as quoted strings in the audit log ("source_count": "10") — unlikerun-check -o json, where they are numbers. Compare withtonumber.
6. What each command costs
Reconciliation runs real queries against real warehouses, and on a consumption-priced warehouse those queries cost money. Know which commands spend before you put one in a loop.
Two things worth knowing specifically:
check-config --db’s table analysis can scale with warehouse size, not suite
size. On platforms with no table-scoped metadata API — BigQuery in particular —
enumerating table metadata walks the datasets rather than jumping straight to
the tables your suite names. On a large warehouse that is slow and not free.
This is why the analysis is confined to the interactive check-config --db and
does not run before every reconciliation.
Gate a large run before it happens. --max-table-rows and
--max-table-bytes make run, run-check and run-drill estimate each
reconciliation’s scan first — a dry run or EXPLAIN, not a scan — and warn when
an estimate exceeds the threshold. Both default to 0 (off) so automated runs
incur no extra queries. BigQuery and Snowflake report bytes; ClickHouse and
PostgreSQL report planner rows; DuckDB, Athena and Databricks advertise no
estimate support, so a gated run there simply emits no warnings.
Keep a local run local. With a stored credential present, a locally executed
run reports its results to Coalesce Quality so they appear in the workspace
alongside backend runs — even when every database in the suite is local. It says
so at INFO, naming the endpoint. Pass --no-report (or set RECON_NO_REPORT)
to resolve no credential and open no connection at all.
7. Investigating a finished run
recheck and drill-deeper take a finished run instead of a suite — either a
local audit-log file or, for a run stored in the workspace, its invocation id.
- Queries are replayed, not re-derived. The audit log holds each query after
variable interpolation and cutoff resolution, so a re-check compares the same
window the original run did — which is what makes “did the gap move?” a
meaningful question. It also means a replay will not pick up an edited query
or variable until you pass
--reresolve.--reresolveis required for anas_ofsnapshot, which can otherwise never turn green. - Only what was left open resumes.
drill-deeperpicks up the segments the previous run stopped at on its row threshold or depth limit;--depthcounts from the depth they already reached, so it means “this many more levels”. Segments that could not be split further, and aggregate groups present on only one side, are reported and skipped rather than silently dropped. An aggregate drill already visits every configured group column, so resuming one needs at least one--add-group-column. recheckre-runs only the reconciliations that did not pass.--allre-runs every one.- Credentials never come from an audit log — it records connection names
only. Pass
--connections, exactly as for a normal run.
--remote to either command to hand the replay to the backend instead,
running it against workspace integrations. --remote needs a stored run’s
invocation id (a local file has nothing server-side to reference) and accepts
--include but not --exclude.
8. Command reference
synq-recon <command> --help is authoritative. This is the map.
Local
Workspace
Deployment edits preserve what you omit.
promote on a re-promote, and
deployment update always, change only the settings you actually pass — so
refreshing a suite snapshot never silently unschedules the deployment or
disables API triggers. Removing a schedule is therefore explicit:
deployment update --clear or promote --clear-schedule. A fresh promote
applies defaults instead: no schedule, not triggerable, drill on.
A deployment’s id is part of every reconciliation’s asset path, so it is
worth keeping stable — rebuilding a deployment from scratch under a new id
re-homes its checks and drops their history. Re-promoting the same suite reuses
its id automatically; promote --deployment-id <id> pins one explicitly when
you are rebuilding.
Flags accepted by every command
9. Worked example
Runnable end to end against the DuckDB fixtures shipped in this repository — no warehouse, no credentials, no network. The suite deliberately contains a difference: one missing order and one with a wrong amount.examples/ (business cases) and
tests/ (one feature each). ./examples/test-all-examples.sh runs
every one.