Common mistakes in dbt packages that break the core design principles of package design — configurability, namespacing, and adapter awareness.
Hardcoded Schema References
-- Bad: works in your project, breaks in everyone else'sFROM my_database.raw_stripe.paymentsThis is the most common anti-pattern. FROM my_database.raw_stripe.payments works in your project because you control the warehouse. Every other user has a different database name, a different schema name, or a different table name.
Fix: Always use source() with var() for schema and database configuration. See packageable model patterns for the full implementation.
-- Good: resolves to whatever the user configuresFROM {{ source('my_package', 'payments') }}sources: - name: my_package schema: "{{ var('my_package_schema', 'raw_stripe') }}" database: "{{ var('my_package_database', target.database) }}"The source() + var() pattern adds a few lines of YAML but makes the difference between a package that installs everywhere and one that only works on your laptop.
Missing Dispatch Implementations
If your package uses SQL that varies by warehouse and you only write a default__ implementation, users on other adapters get unexpected behavior or errors.
-- Only works on Snowflake/Postgres{% macro default__my_date_trunc(datepart, date_expression) %} DATE_TRUNC('{{ datepart }}', {{ date_expression }}){% endmacro %}BigQuery’s DATE_TRUNC takes arguments in a different order: DATE_TRUNC(date_expression, datepart). A BigQuery user installing this package gets a silent wrong-result bug or a syntax error depending on the expression.
Fix: Add dispatch implementations for every adapter you claim to support. If your README says “works on Snowflake, BigQuery, and Redshift,” test on all three.
{% macro bigquery__my_date_trunc(datepart, date_expression) %} DATE_TRUNC({{ date_expression }}, {{ datepart }}){% endmacro %}Before writing custom dispatch macros at all, check whether dbt Core already provides a built-in. Since dbt-utils v1.0, cross-database macros like datediff, dateadd, safe_cast, and hash live in the dbt namespace. {{ dbt.datediff(...) }} handles adapter differences for you.
Tight Version Constraints
# Forces every user to use exactly this versionpackages: - package: dbt-labs/dbt_utils version: "0.20.1"Pinning to a specific version forces every user to use exactly that version and creates dependency conflicts with other packages. If another package requires dbt-utils >=1.0.0, and yours pins to 0.20.1, dbt deps fails with a version conflict that the user has to debug.
Fix: Use the widest version ranges that actually work.
packages: - package: dbt-labs/dbt_utils version: [">=0.20.0", "<1.0.0"]Test your package against the minimum and maximum of the range in your CI matrix. If it passes at both endpoints, the range is safe.
Generic Model Names
A model called customers will collide with the user’s own customers model. A model called daily_summary will collide with any other package that thought daily_summary was a good generic name.
-- Collision waiting to happen-- models/customers.sqlSELECT * FROM {{ source('my_package', 'customers') }}Fix: Prefix everything with your package name.
-- models/my_package__customers.sqlSELECT * FROM {{ source('my_package', 'customers') }}This applies to all model types: base, intermediate, and mart. See namespacing patterns for the full convention. The verbosity is a feature — revenue_tools__monthly_mrr is unambiguous in a way that monthly_mrr never will be.
Table Materialization by Default
models: my_package: +materialized: tableWhen someone runs dbt deps && dbt run, your package shouldn’t create 30 physical tables in their warehouse. Tables consume storage, take longer to build, and create warehouse objects the user didn’t ask for.
Fix: Default to view in the package’s dbt_project.yml. Let users override for performance.
models: my_package: +materialized: viewUsers who want table materialization can override in their own project:
models: my_package: +materialized: tableThis is the opposite of the normal guidance for regular projects, where table is the recommended default. The difference is ownership: in your own project, you want debugging visibility and query performance. In someone else’s project, you want a light footprint they can customize.
No require-dbt-version
name: 'my_package'version: '0.1.0'# require-dbt-version is missingWithout require-dbt-version, users on incompatible dbt versions get cryptic compilation errors instead of a clear message. A user on dbt 1.1 trying to use a feature from dbt 1.6 sees a Jinja compilation failure that looks like a bug in your package rather than a version incompatibility.
Fix: Always set require-dbt-version with a range that covers the versions you’ve actually tested.
require-dbt-version: [">=1.3.0", "<3.0.0"]With the Fusion engine (dbt 2.0) now available, [">=1.3.0", "<3.0.0"] is a reasonable default that covers both dbt Core 1.x and dbt 2.x. Test at both endpoints in CI to make sure.
Bonus: Not Documenting Variable Names
This isn’t a code anti-pattern, but it’s equally common. If your package has 15 var() calls and the README doesn’t list them, users have to read your source code to figure out how to configure the package.
Fix: Document every variable in your README with its name, purpose, and default value. A table works well:
| Variable | Description | Default |
|---|---|---|
my_package_schema | Schema where source data lives | 'my_data' |
my_package_database | Database for source data | target.database |
my_package__daily_summary_enabled | Enable daily summary model | true |
Users should be able to configure your package from the README without reading a single SQL file.