Skip to main content

zhao logo

zhao-dbt-plan

Crates.io License

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 --anchor weren'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)

Table of built distributions (wheels) for zhao-dbt-plan 0.2.1
File
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 log

Release 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 log

Release 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 log

Release 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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.1 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page