Why we built dbt-doctor
Why we built dbt-doctor, an offline static analysis tool that turns recurring dbt project issues into clear checks and a practical health score.

The dbt projects I reviewed tended to surface the same categories of issues. Missing contracts. Incremental models without a unique_key. Sources without freshness configuration. Model SQL that bypassed dbt lineage by referencing relations directly.
The findings were predictable. The tooling to catch them consistently was not.
The existing options each solve part of the problem. SQLFluff handles SQL style. dbt-checkpoint enforces pre-commit checks. dbt-score grades metadata quality. dbt-project-evaluator catches structural issues. Using all of them means maintaining separate configurations, separate rule sets, and several places where something can break in CI.
Most teams use one or two tools and leave the rest uncovered.
The gap was not another linter
The gap that bothered me most was not only fragmentation. It was that most static analysis output means very little outside the engineering team.
A 200-line linting report communicates nothing to an engineering lead deciding whether to invest in technical debt cleanup. A list of YAML violations means nothing to a client asking whether their dbt project is in good shape.
The inspiration came from React Doctor. One command, no application build required, and an immediate diagnosis with a clear health score. Applying that model to dbt seemed useful and missing.
dbt-doctor adapts that idea to dbt projects.
What dbt-doctor does differently
The design decisions came from two directions: what engineers need and what non-technical stakeholders can actually read.
Zero friction for engineers
The first requirement was a useful offline mode. dbt-doctor can inspect a project without a warehouse connection, dbt compilation, or credentials. That makes it useful immediately after cloning a repository, before the local environment is fully configured.
Run it from the dbt project root:
npx dbt-doctor@latest --offline
The offline flag skips the remote score API and share link. The project is still scanned and scored locally.
A score stakeholders can read
The second requirement was a result that works outside an engineering conversation. dbt-doctor returns a score from 0 to 100 with three labels:
- Great: 75 or above
- Needs work: 50 to 74
- Critical: below 50
Engineers can inspect the detailed findings. Everyone else can understand whether project health is improving or degrading. "The project moved from 41 to 78" works in a delivery update as well as a technical retrospective.
The default scoring mode accounts for three things:
score = max(0, round(100 - rule penalty - file spread penalty - finding volume penalty))
Unique error rules cost more than warning rules. Problems spread across many files receive a larger penalty. Finding volume adds a capped penalty so one repeated issue does not dominate the score indefinitely.
This keeps the score focused on systemic quality while still reflecting the blast radius of the findings. Teams that want scoring focused on rule diversity can switch to unique-rules mode.
What it checks
The checks focus on maintainability, project structure, documentation, tests, source configuration, governance and SQL quality. The current catalog is published in the dbt-doctor rules reference.
A few checks came directly from recurring findings in real projects.
Models without enforced contracts
A column rename in a model feeding a dashboard can break downstream consumers without a clear boundary. The model-contract-enforced rule flags models that do not set contract.enforced: true in model YAML.
Dependencies that bypass dbt lineage
Hardcoded relation names make lineage and environment isolation harder to trust. The prefer-ref-over-raw-source rule detects model SQL that appears to reference a relation directly instead of using ref().
Incremental models without a unique key
Incremental merge strategies depend on a stable key. The incremental-unique-key rule flags incremental models that do not declare unique_key in SQL config or model YAML.
Sources without freshness configuration
Freshness monitoring needs explicit thresholds. The source-freshness rule flags declared sources without a freshness configuration and recommends adding warn_after and error_after thresholds.
SQL quality when the team wants it
dbt-doctor can run SQLFluff when it is available and can adopt an existing .sqlfluff or pyproject.toml configuration. Teams that only want the built-in structural checks can use skipSqlfluff or customRulesOnly.
CI integration
The GitHub Action was part of the product design. A check that only runs locally catches issues when someone remembers to run it. A check that runs on every pull request catches them before they reach production.
- uses: northgraindata/dbt-doctor@main
with:
diff: main
github-token: ${{ secrets.GITHUB_TOKEN }}
With diff: main, only files changed against the base branch are scanned. With a GitHub token on a pull request, findings appear as a sticky PR comment. The action also exposes the health score for follow-up quality gates.
The complete inputs and outputs are documented in action.yml.
What comes next
The check library should expand from real findings, not from abstract ideas about what a good dbt project looks like.
The repository is open source at github.com/northgraindata/dbt-doctor. The rule catalog is generated from the implementation. Pull requests for useful checks, issues for false positives, and feedback from real projects are all valuable.
Run it on a dbt project first. If it catches something useful, a GitHub star helps more teams discover it.
