Template variables
A variable is resolved once per run and interpolated into both sides, so the two queries are guaranteed to use the same boundary.where:, so you do not have to switch to raw SQL to use them.
What you can write inside {{ }}
Offsets subtract from
now, today or yesterday, in units of d (days), h (hours), m (minutes) or w (weeks). Only subtraction is supported, because a comparison window must not go into the future.
Override any variable for a single run:
Windows
Awindow: restricts a reconciliation to a recent period without you writing the predicate:
lookbackis required, and positive: how far back from the run the window reaches, in any of the duration forms ("14d","P14D","14 days",{count: 14, period: day}). A zero or negative lookback is ignored, with a warning, and every row is compared.columnis the timestamp or date column each table side is filtered on. The filter is written for the column’s type, so aDATEcolumn is compared with a date, and it is added to anywhere:you wrote rather than replacing it.strategy: sliding(the default) measures back from the run’s reference time, so each run compares a moving fortnight.fixedends the window at the last period boundary in UTC and starts it one lookback earlier, so consecutive runs inside the same period compare exactly the same rows. The period follows the lookback: midnight for a whole number of days, the start of the hour for whole hours, otherwise the minute.
fixed when you want two runs to be comparable to each other, not just each correct in isolation.
Every window also defines two template variables for its own reconciliation: {{ window_start }}, and {{ window_end }} (the run’s reference time, or the period boundary for a fixed window). Use them where the column filter does not reach:
-
A
query:side is never filtered on the column. You wrote its SQL, so filter it yourself: -
A
where:that already references{{ window_start }}or{{ window_end }}is kept exactly as written, and the column is not filtered a second time on that side. A window without acolumnfilters nothing on its own and only defines the variables.
MAX() over a large table scans only the recent rows.
Durations
window.lookback, cutoff.offset, the timeouts and error_handling durations all take the same forms:
A leading
- makes a duration negative, which is what a cutoff offset usually is. Years and months are refused, because they are not a fixed length: write the days instead, for example P30D. A duration longer than the time since 1970 still loads, with a warning, since it is almost always a wrong unit or a typo. synq-recon suite yaml and the app write durations back in the short forms of the first two rows (14d, 1h30m), which every CLI release reads.
Cutoffs for a lagging target
This is the important one, and the one people reach for last when they should reach for it first. If the target is behind the source (a CDC stream, a batch load, a read replica), the most recent rows are on the source and not yet on the target. They are not missing. They are in flight. A reconciliation with no cutoff reports them as a difference, every time, and the difference moves as the lag moves. Acutoff: derives a watermark from the data itself, then filters both sides to at-or-below it. Rows in flight are excluded from the comparison rather than reported as errors.
The cutoff is stricter than the lag: 415 rows, not 470.
truncate: HOUR moves the watermark down to the start of the hour, and offset: "-5m" moves it 5 minutes further back. Both give a safety margin, because a pipeline that writes slightly out of order can write a row below a watermark that it already passed.Three levels of control
- Simple
- Different column per side
- Full control
One column, both sides:Each side’s watermark is
MAX(created_at) over its own dataset; the lower of the two becomes the cutoff.How a cutoff resolves
1
Derive a watermark per configured side
MAX (or MIN) of the named column, or the result of your custom query.2
Combine the two into one value
combine: min by default.3
Truncate to a boundary
Optional. Only for a timestamp watermark.
4
Apply the offset
Optional, after truncation. Negative buys margin.
5
Build the WHERE clause for each side
Against the derived column, or whatever
apply: names.WHERE clause of each side. Thus you can always find the comparison window of a run later. In the app, the Overview of a run shows them under Cutoff. This is the quickest way to check that an offset or truncate does what you want.
Time travel
as_of compares a snapshot at a point in time, not the current state. Snowflake, BigQuery and Databricks support it:
column still filters a table side that has as_of, applied to the snapshot. If the side lists columns, the window column has to be one of them.
A re-check replays the original run’s queries, which for an
as_of reconciliation means it re-compares the same frozen snapshot and can therefore never turn green. Pass --reresolve to re-derive the queries against current data.Choosing between them
Use a cutoff when
The two systems are the same data at different times. You want to compare what both sides have, and ignore what is still on its way.
Use a window when
History does not change and only recent data is important to you. This is usually for cost, not for correctness.
Use variables when
You need an explicit, reproducible boundary in your own SQL, identical on both sides.
Use as_of when
You need a specific historical state, not “now”.
Next
Running locally
Overriding variables per run, and reading what a run resolved them to.
Investigating results
Confirming a difference is real, and re-checking after a fix.