zhao-dbt-plan
A static microbatch cascading time-window planner for dbt. dbt's microbatch incremental
strategy applies one flat --event-time-start/--event-time-end window across an entire
selection — so when a rolling-window model reads a wider span than its immediate upstream was
recomputed for, dbt has no way to know it needs a wider batch too. zhao-dbt-plan reads your
compiled manifest, walks the DAG within whatever you --select, and computes the correct,
per-model expanded window — as a plan you review, never a command it runs for you.
$ zhao-dbt-plan --select tag:microbatch_demo --event-time-start 2026-07-01 --event-time-end 2026-07-01 --pretty
[layer 0] mb_orders_daily [2026-07-01 .. 2026-07-01]
[layer 1] mb_orders_rolling_7d [2026-06-28 .. 2026-07-05]
[layer 2] mb_orders_rolling_14d [2026-06-26 .. 2026-07-06]
[layer 3] mb_orders_summary [2026-06-25 .. 2026-07-07]
[layer 3] mb_orders_wide_lookback [2026-04-07 .. 2026-07-11]
warning: mb_orders_wide_lookback: expanded window (96 days) exceeds max_window_expansion_days (90)
Why
If Model A aggregates a 7-day trailing window and Model B reads Model A over a further
[-3, +4] window, a backfill to Model A on day T silently corrupts Model B's outputs from
T-4 through T+3 — and native dbt only ever re-triggers Model B for day T itself. Manually
widening the whole selection's window instead wastes compute recomputing everything that wasn't
actually touched. zhao-dbt-plan computes the real, minimal per-model window instead.
It never executes anything. Not dbt build, not dbt run — plan-only, permanently. You
decide how to actually run the plan: raw dbt, Dagster, Airflow, a Databricks Asset Bundle,
whatever you already use.
Install
macOS / Linux — Homebrew:
brew install allenhori/zhao/zhao-dbt-plan
Windows — Scoop:
scoop bucket add zhao https://github.com/allenhori/zhao-scoop
scoop install zhao-dbt-plan
Python / dbt projects — PyPI, with uv or pip:
uv tool install zhao-dbt-plan # or: pip install zhao-dbt-plan
A pre-built binary in a wheel, no Rust toolchain needed — pin it next to dbt-core in your
project's own dependencies so local dev and CI get the same version.
Any platform — the install script:
curl -fsSL https://raw.githubusercontent.com/allenhori/zhao-dbt-plan/master/scripts/install.sh | sh
Update with brew upgrade zhao-dbt-plan, scoop update zhao-dbt-plan, or
uv tool upgrade zhao-dbt-plan / pip install --upgrade zhao-dbt-plan; re-run the install
script if you used that. Homebrew, Scoop and PyPI carry stable releases only.
Rust users: cargo install zhao-dbt-plan (via
crates.io).
Usage
cd your-dbt-project
zhao-dbt-plan --select tag:daily --event-time-start 2026-07-01 --event-time-end 2026-07-01
--select/--exclude are forwarded verbatim to dbt ls — dbt's own selector engine, not a
reimplementation, so anything real dbt build --select ... accepts here works identically
(tags, paths, + graph operators, intersections, the works).
With no --event-time-start/--event-time-end, every Entry Node in the selection defaults to
yesterday — meant to be run daily (e.g. as the first step of a cron/CI job), so the dates stay
current. Whenever this default-yesterday path is taken, a note is printed to stderr
unconditionally (not gated behind --pretty) so the assumption is never silent:
note: --event-time-start/--event-time-end not supplied, defaulting every Entry Node to yesterday (2026-08-07)
The same note is also recorded in the JSON's metadata.anchor_window.note, and shown as a
banner in the --html report's header — see --anchor below for the one case where this
default never applies.
A model opts into cascading expansion via config.meta.zhao in its own {{ config(...) }}:
{{ config(
materialized='incremental',
incremental_strategy='microbatch',
event_time='order_date',
batch_size='day',
meta={'zhao': {'lookback': 3, 'lookahead': 4}}
) }}
lookback/lookahead default to days; set lookback_unit/lookahead_unit (day, week,
month, or year) if you need a different one — e.g. {'lookback': 3, 'lookback_unit': 'month'} for "3 calendar months back." Each direction has its own independent unit.
No config.meta.zhao block at all means zero expansion for that model — deliberately, so a
forgotten declaration is visibly a no-op in the plan, not a silently inherited default.
Per-upstream overrides (different lookback depending on which upstream) via
lookback_overrides/lookahead_overrides, keyed by the upstream model's bare name, e.g.
{'lookback_overrides': {'orders': 5}} to give just the orders upstream a 5-day lookback
while every other upstream keeps the model's own default.
Every model in the plan (JSON and --pretty) carries a layer: its longest-path depth from an
Entry Node within the selected subgraph. An Entry Node is layer: 0; every other model is
1 + max(every upstream's layer) — a diamond dependency (two upstream paths of different
length) still collapses to one number, the longer path's +1. Lets you read the DAG's tier
structure straight off the plan without tracing depends_on by hand.
--anchor <model>: pinning the literal window to a specific model
By default, the literal --event-time-start/--event-time-end window (or the default-yesterday
fallback) applies to every Entry Node in the selection — a selected model with no upstream
dependency within the selection — and cascades forward from there. --select '+model_c+'
(or +model_c, or model_c+) does not, by itself, pin the literal window on model_c —
it still applies to whichever selected model(s) have no upstream dependency within the
selection, which may be several hops upstream of model_c.
--anchor <model> pins the literal window on that one named model instead, wherever it sits in
the selected subgraph:
zhao-dbt-plan --select '+mb_orders_rolling_14d+' --anchor mb_orders_rolling_14d \
--event-time-start 2026-01-01 --event-time-end 2026-01-31
- Downstream of the anchor: the same forward-cascade formula as always, just starting from the anchor's window instead of an Entry Node's.
- Upstream of the anchor: walked backward, one edge at a time, applying the same formula
in reverse — at each hop, the upstream model's needed window is the downstream
(closer-to-anchor) model's own window, padded outward by that downstream model's own
(lookback, lookahead)config. Per-upstream overrides and multi-path bounding-box union both apply with the same precedence they already have going forward. - Anything in the selection with no path to/from the anchor: untouched, using the normal
Entry-Node-based algorithm exactly as if
--anchorweren't passed.
--anchor is a single bare model name — not inferred from --select's +/graph-operator shape,
deliberately, so this addon never needs to parse any part of dbt's own selector grammar (the
same reason --select/--exclude are forwarded verbatim to dbt ls rather than reimplemented).
It must name a model actually present in --select's resolved selection (post dbt ls), or
this fails with a clear error naming both the requested anchor and what was actually selected.
--event-time-start/--event-time-end become mandatory when --anchor is used — there is
no yesterday-default on this path. --anchor is a deliberate, occasional, investigative
operation (fixing a known bad date range), where silently defaulting to yesterday on a forgotten
date flag would confidently compute a plan for the wrong window with no error at all.
The plan JSON's metadata records which model was named, if any: anchor_model (omitted
entirely, not null, when --anchor wasn't given).
--html: an interactive visual report
zhao-dbt-plan --select tag:daily --event-time-start 2026-07-01 --event-time-end 2026-07-01 --html
Opt-in, like --pretty — never generated unless --html is passed, since most runs (typically
CI, disposable) don't need it. Writes a self-contained, interactive HTML file to
<project-dir>/target/zhao/dbt-plan/dbt_plan_<YYYYMMDDHHMMSS>.html (UTC, timestamped so repeat
runs never collide and nothing needs cleaning up) — a directory distinct from wherever the JSON's
--output-file goes, and it never changes the JSON's own default path or contents.
Open a live demo (rendered via htmlpreview.github.io, since GitHub shows raw HTML as source rather than rendering it — the file itself is also there to download and open locally).
Each model is a node showing its full name, computed date-range window, and layer, laid out by layer with its downstream connections drawn so the cascading structure is visible without reading JSON at all. All interactivity (search, highlighting the upstream/downstream chain on selection, a resizable side panel) runs client-side in plain JavaScript against an embedded JSON blob — no network access, no CDN reference anywhere, works fully offline. Model names are never truncated, regardless of length — node boxes wrap to fit the full name rather than clipping it.
Making the plan actually apply: zhao_utils
zhao-dbt-plan computes the correct window — but on its own, it can't make dbt's compiled SQL
actually use it. dbt's own microbatch ref() filtering can't be overridden by a project macro,
so a plain ref() to a widened upstream still gets dbt's default single-batch window, silently.
zhao_utils is a small, separately-licensed
(Apache-2.0), separately-repo'd dbt package — wref() ("windowed ref"), a drop-in ref()
replacement, plus two boundary helpers — that closes that gap, reading the exact same meta.zhao
block this planner already does. Completely optional: it's for whoever's starting the
rolling-window pattern fresh, and zhao-dbt-plan itself works identically without it. See its
own README for install/usage.
Wiring it into your workflow
zhao-dbt-plan reads the same top-level zhao.yml keys as zhao-cli (dbt-command,
dbt-args, against), so if zhao-cli is already set up for your project, the dbt invocation
is already right. Have a coding agent do the rest (local and CI, any CI system):
Read https://raw.githubusercontent.com/allenhori/zhao-cli/master/docs/ai-setup.md and set up zhao for this repository. Then read https://raw.githubusercontent.com/allenhori/zhao-dbt-plan/master/docs/ai-setup.md and add zhao-dbt-plan.
The addon-specific guide is docs/ai-setup.md. Do the zhao-cli guide first;
this one only covers what zhao-dbt-plan adds.
Flag reference
| Flag | Default | What it does |
|---|---|---|
-s, --select <selector> |
— | Required. dbt's own selector syntax, forwarded verbatim to dbt ls --select (tags, paths, + graph operators, intersections — anything dbt build --select accepts). |
--exclude <selector> |
— | Forwarded verbatim to dbt ls --exclude, same syntax as --select. |
--event-time-start <date> |
yesterday | Explicit Anchor window start (YYYY-MM-DD). Must be passed together with --event-time-end, or not at all. |
--event-time-end <date> |
yesterday | Explicit Anchor window end. Mandatory (both this and --event-time-start) whenever --anchor is used — see below. |
--anchor <model> |
— | Pins the literal window to this one named model instead of every Entry Node — see --anchor above. |
--project-dir <dir> |
. |
The dbt project directory. Everything else (manifest path, dbt ls/dbt parse invocations) is resolved relative to this. |
--manifest <path> |
<project-dir>/target/manifest.json |
Path to the compiled manifest to read. |
-o, --output-file <path> |
<project-dir>/target/zhao/dbt_plan.json |
Destination for the plan JSON. |
--pretty |
— | Also renders an ASCII tree of the plan to the terminal ([layer N] name [start .. end]), in addition to writing the JSON. |
--html |
— | Also writes the self-contained, interactive HTML report — see --html above. |
--dbt-command <cmd> |
dbt |
Executable/prefix for every internal dbt call this addon makes (dbt parse for manifest freshness, dbt ls for selection). Shell-word-split, so a multi-word wrapper ("uv run dbt", "myshell custom-flag") works as a genuine prefix. Overrides zhao.yml's top-level dbt-command (shared with zhao-cli) when given. |
--dbt-args "<string>" |
— | Extra arguments appended to every internal dbt call, e.g. "--target ci". Overrides zhao.yml's top-level dbt-args when given. |
--against <ref> |
master |
The ref state:-method selectors are compared against, when --state isn't given explicitly. Overrides zhao.yml's top-level against (shared with zhao-cli's own git-native Baseline resolution) when given. |
--state <dir> |
— | An explicit, already-compiled manifest directory to pass as dbt ls --state, for a state:-method selector — resolves git-natively (merge-base against --against, compiled in a temporary worktree) when omitted and the selector actually needs one. Wins outright over --against if given. |
-h, --help |
— | Full flag reference, same as this table, printed from the binary itself. |
-V, --version |
— | Print version. |
zhao.yml's dbt-plan: block also has its own, addon-specific max-window-expansion-days key
(default 90, warn-only — the ceiling mb_orders_wide_lookback trips in the example above),
with no CLI flag equivalent; it has no equivalent concept in zhao-cli to share a top-level key
with. The plan JSON's shape is documented inline in src/output.rs.
Compatibility
Tested against both dbt-core (1.10+) and dbt Fusion (2.0 preview) — both produce the same
manifest.json schema (dbt_schema_version: manifest/v12.json) this reads, and both are
verified to produce byte-identical plans from the identical project (see
tests/end_to_end.rs's dbt_core_and_dbt_fusion_produce_identical_plans).
Building a zhao-cli Addon
zhao-dbt-plan is zhao's first Addon — a standalone binary with no technical dependency on
zhao-cli (this binary runs entirely on its own, as shown above, with zero zhao-cli install
required). It's also discoverable as zhao dbt-plan once installed alongside zhao-cli on the
same PATH (this install.sh installs to the same ~/.zhao/bin directory zhao-cli uses,
specifically for that) — zhao-cli finds any zhao-<name> binary on PATH and dispatches to it,
forwarding all arguments and the exit code, communicating purely through files. So
zhao dbt-plan --select ... and zhao-dbt-plan --select ... (standalone, as in every example
above) are equivalent once both are installed. The whole contract is deliberately just a
subprocess plus files — no shared library, no compiled-in knowledge on zhao-cli's side of
any specific Addon, discovery purely by the zhao-<name> naming convention on PATH. See
zhao-cli's own examples/hello-zhao-addon/ for a minimal reference implementation of the
same contract, if you want to build your own Addon.
License
AGPLv3 — see LICENSE. Contributions require a signed CLA (see CONTRIBUTING.md) so this project can keep offering a commercial license alongside the open one.
Status
Early. zhao-core/zhao-cli (Apache 2.0) are the format-agnostic engine and CLI this addon
extends — see github.com/allenhori/zhao-cli.
Release files for zhao-dbt-plan 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| zhao_dbt_plan-0.2.1-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| zhao_dbt_plan-0.2.1-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| zhao_dbt_plan-0.2.1-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 3.2 MB
Release files / zhao_dbt_plan-0.2.1-py3-none-win_amd64.whl
| Download URL | zhao_dbt_plan-0.2.1-py3-none-win_amd64.whl |
|---|---|
| Size | 600.2 kB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
8273e1d934ae16fda404b6f363126870808349e751ae592d9a72ba2047253756
|
|
BLAKE2b-256 checksum How to use checksums |
a5e7f7768f7d0a4dae50e79196bb448c6d673dc63ae6013f4e8a085c66e31652
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 684.8 kB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
2d3f2dabed8256dc5ad643b922209a71fcab073c4ade9bcb5154afa321d26d85
|
|
BLAKE2b-256 checksum How to use checksums |
dcea582c1a469071f13e8e878e6e17a77d2858c28860da61ba35dcb6d0711839
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | zhao_dbt_plan-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 641.9 kB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
aeebe90e8284a2d2c04ddc0c37cb1e512ea14988d905cc6428f042469f61dcdb
|
|
BLAKE2b-256 checksum How to use checksums |
0ffb4fdef21e31bf6f88dedd879ba10f4f46fca466c7f47532c7ccfdfe4adc38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / zhao_dbt_plan-0.2.1-py3-none-macosx_11_0_arm64.whl
| Download URL | zhao_dbt_plan-0.2.1-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 628.3 kB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
9bb7d0fa9ff2843f5f7f4bdc0bfbe333a5d10cd7372b6665804e4272bda361e5
|
|
BLAKE2b-256 checksum How to use checksums |
3c8affe539da247afc6bd058e6025ebfab0b869efbf00da360e1949eb2acd353
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / zhao_dbt_plan-0.2.1-py3-none-macosx_10_12_x86_64.whl
| Download URL | zhao_dbt_plan-0.2.1-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 653.6 kB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
a150fc8fa7e951b67d5f41a54298095da01dabb966990f50c77fbbde3b6b6bda
|
|
BLAKE2b-256 checksum How to use checksums |
16a350a2c271a3e779dff6c245bfd05588e4cc5c016d73451ec4f4440b59fd74
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log