ServicesAboutNotesContact Get in touch →
EN FR
Note

dbt Packageable Model Patterns

Three patterns that make dbt models installable by anyone — configurable sources with var(), enable/disable flags, and namespaced model names.

Planted
dbtdata modelingdata engineering

Three techniques make dbt models installable by anyone: configurable sources with var(), enable/disable flags, and namespaced model names. The Fivetran team’s approach to these patterns has become a de facto standard. Omitting any one of them will break installations in environments with different schema layouts or naming.

Configurable Sources with var()

Never hardcode where source data lives. What’s raw_stripe.payments in your warehouse is analytics.stripe_data.payment_events in someone else’s. The var() function with sensible defaults handles this cleanly.

Source Definition

models/base/_sources.yml
sources:
- name: my_package
schema: "{{ var('my_package_schema', 'my_data') }}"
database: "{{ var('my_package_database', target.database) }}"
tables:
- name: events
identifier: "{{ var('my_package_events_identifier', 'events') }}"

Three levels of configurability here:

  1. Schema — where the source tables live. Most users will need to change this.
  2. Database — defaults to the user’s target database. Only needed for cross-database setups.
  3. Identifier — the actual table name. Handles cases where a table has a different name in someone’s warehouse (raw_events instead of events).

Base Model

-- models/base/base__my_package__events.sql
WITH source AS (
SELECT
event_id,
event_name,
event_timestamp,
user_id
FROM {{ source('my_package', 'events') }}
)
SELECT
event_id,
event_name,
event_timestamp,
user_id
FROM source

The base model references source(), which resolves the schema, database, and identifier from the YAML definition. Users point the package at their own schema by setting my_package_schema in their dbt_project.yml:

# User's dbt_project.yml
vars:
my_package_schema: 'raw_stripe'
my_package_events_identifier: 'payment_events'

This is the same base layer pattern used in regular projects, with var() wrapping the parts that differ across environments.

Variable Naming Convention

Follow the convention established by Fivetran and dbt Labs:

SettingVariable NameExample
Schema{package}_schemamy_package_schema
Database{package}_databasemy_package_database
Table identifier{package}_{table}_identifiermy_package_events_identifier
Feature flag{package}__{model}_enabledmy_package__daily_summary_enabled

The double underscore before the model name in feature flags separates the package namespace from the setting name, avoiding ambiguity with multi-word package names.

Enable/Disable Models

Not every user needs every model in your package. Enable/disable flags let users turn off parts they don’t need, reducing build time and warehouse clutter.

-- models/marts/my_package__daily_summary.sql
{{ config(enabled=var('my_package__daily_summary_enabled', true)) }}
SELECT
DATE(event_timestamp) AS event_date,
COUNT(DISTINCT user_id) AS unique_users,
COUNT(*) AS total_events
FROM {{ ref('base__my_package__events') }}
GROUP BY 1

The enabled config accepts a var() call directly. Default to true so models are included unless explicitly disabled. Users opt out in their dbt_project.yml:

vars:
my_package__daily_summary_enabled: false

When a model is disabled, dbt skips it entirely — no compilation, no execution, no warehouse object. Downstream models that reference a disabled model will fail, so document which models are safe to disable independently. A common pattern is to group models into “modules” with a single flag:

vars:
my_package__daily_models_enabled: true
my_package__weekly_models_enabled: true
my_package__monthly_models_enabled: false # Disable all monthly aggregations

Namespaced Model Names

Every model in your package should start with the package name. If your package is called revenue_tools, name your models revenue_tools__monthly_mrr and revenue_tools__churn_events, not monthly_mrr and churn_events.

This prevents naming collisions when users have multiple packages installed. Without prefixes, two packages that both define a customers model will conflict, and the error messages aren’t always clear about the cause.

The convention applies to all model types:

LayerExample
Basebase__my_package__events
Intermediatemy_package__enriched_events
Martmy_package__daily_summary

Base models follow the standard base naming convention (base__source__entity) with the package name as the source. Higher-layer models use the package name as a prefix directly.

This is verbose, but collisions between packages are one of the most frustrating debugging experiences in dbt. A user with fivetran_stripe, fivetran_shopify, and your revenue_tools package installed needs unambiguous model names.

Putting It Together

Here’s a complete source and base model setup for a package called ad_analytics:

models/base/_sources.yml
sources:
- name: ad_analytics
schema: "{{ var('ad_analytics_schema', 'raw_ads') }}"
database: "{{ var('ad_analytics_database', target.database) }}"
tables:
- name: campaigns
identifier: "{{ var('ad_analytics_campaigns_identifier', 'campaigns') }}"
- name: ad_spend
identifier: "{{ var('ad_analytics_ad_spend_identifier', 'ad_spend') }}"
-- models/base/base__ad_analytics__campaigns.sql
WITH source AS (
SELECT *
FROM {{ source('ad_analytics', 'campaigns') }}
),
renamed AS (
SELECT
campaign_id,
campaign_name,
platform,
start_date,
end_date,
budget_amount,
currency_code
FROM source
)
SELECT * FROM renamed
-- models/marts/ad_analytics__daily_spend.sql
{{ config(enabled=var('ad_analytics__daily_spend_enabled', true)) }}
SELECT
DATE(spend_date) AS spend_date,
c.campaign_name,
c.platform,
SUM(s.amount) AS total_spend,
SUM(s.impressions) AS total_impressions,
SUM(s.clicks) AS total_clicks
FROM {{ ref('base__ad_analytics__ad_spend') }} s
LEFT JOIN {{ ref('base__ad_analytics__campaigns') }} c
ON s.campaign_id = c.campaign_id
GROUP BY 1, 2, 3
# dbt_project.yml (package defaults)
vars:
ad_analytics_schema: 'raw_ads'
ad_analytics_database: null
ad_analytics__daily_spend_enabled: true

Users customize with minimal configuration:

# User's dbt_project.yml
vars:
ad_analytics_schema: 'my_company_ads'

Everything else falls through to defaults. The user gets a working package with one line of configuration.