dbt Variables with var(): Parameterizing Models the Right Way
Hardcoding a date cutoff, a threshold, or a business rule directly into a modelโs SQL works fine until the day it needs to change โ and then youโre hunting through every model that has that value baked in, hoping you find all of them. dbtโs var() function exists to pull values like this out of your SQL and into a single, declared, overridable place.
Declaring Variables
Variables are declared with defaults in dbt_project.yml, giving every model in the project a shared, single source of truth.
vars: start_date: '2024-01-01' churn_threshold_days: 30 excluded_customer_ids: [1001, 1042, 2087]Reading Variables in a Model
Inside any model, var() retrieves the value by name:
select *from {{ ref('stg_orders') }}where order_date >= '{{ var("start_date") }}' and customer_id not in ( {% for id in var("excluded_customer_ids") %} {{ id }}{% if not loop.last %},{% endif %} {% endfor %} )Because var() returns whatever type the variable was declared as โ a string, a number, a list โ it composes naturally with the loops and conditionals covered in Jinja Templating.
Overriding Variables at Run Time
The real power of var() isnโt the default in dbt_project.yml โ itโs that any run can override it from the command line without touching a single file.
# Uses the default from dbt_project.ymldbt run --select customer_orders
# Overrides start_date just for this rundbt run --select customer_orders --vars '{"start_date": "2025-06-01"}'This is what makes backfills, one-off reprocessing, and environment-specific behavior possible without maintaining separate copies of a model. A backfill job for a specific historical window is just a different --vars flag on the same command everyone else runs.
Providing a Fallback Default Inline
var() accepts a second argument that acts as a fallback if the variable isnโt declared anywhere at all โ useful for optional configuration that most environments donโt need to think about.
select *from {{ ref('stg_events') }}where event_date >= '{{ var("lookback_start_date", "2020-01-01") }}'If lookback_start_date isnโt set in dbt_project.yml or passed via --vars, this falls back to '2020-01-01' instead of raising a compilation error โ the safer default when a variable is genuinely optional rather than required.
Variables vs. Environment Variables
Itโs easy to conflate var() with env_var(), but they solve different problems. var() is for values that change per-run or per-purpose within a single dbt invocation (a date cutoff, a feature flag). env_var() is for values tied to the execution environment itself โ most commonly secrets and connection details that shouldnโt live in version control at all.
-- var(): a business-logic parameter, safe to commitwhere order_date >= '{{ var("start_date") }}'
-- env_var(): typically used in profiles.yml for credentials, not business logicpassword: "{{ env_var('DBT_WAREHOUSE_PASSWORD') }}"A useful rule: if the value could reasonably be committed to Git, itโs a var(). If itโs a secret or environment-specific infrastructure detail, itโs an env_var().
A Practical Pattern: Feature-Flagging a Model Change
Variables are a clean way to roll out a risky model change gradually, without maintaining two parallel model files.
{% if var("use_new_attribution_logic", false) %}
select customer_id, new_attribution_model(events) as attributed_channel from {{ ref('stg_events') }}
{% else %}
select customer_id, legacy_attribution_channel as attributed_channel from {{ ref('stg_events') }}
{% endif %}Running with --vars '{"use_new_attribution_logic": true}' in a staging environment lets you validate the new logic before flipping the default in dbt_project.yml for everyone.
Variables Scoped to a Specific Model
Global vars in dbt_project.yml apply everywhere by default, but you can scope overrides to specific model paths if only part of the project needs a different value.
models: my_project: finance: +vars: start_date: '2023-01-01'This is useful when different domains of the project genuinely need different defaults for the same variable name, without every model author needing to remember to pass --vars manually for that folder.
Variables in Tests and Macros, Not Just Models
var() isnโt limited to model files โ it works identically inside custom generic tests and macros, which is useful for making a shared threshold configurable project-wide instead of hardcoded inside a test macro.
-- macros/test_value_within_range.sql{% test value_within_range(model, column_name) %}
select *from {{ model }}where {{ column_name }} < {{ var("min_acceptable_value", 0) }} or {{ column_name }} > {{ var("max_acceptable_value", 1000000) }}
{% endtest %}This lets a single test macroโs tolerance be tuned per environment or per run via --vars, without editing the macro itself โ the same overridability that makes var() valuable in models extends cleanly into the testing layer.
Common Mistakes
Using var() for secrets. A variable declared in dbt_project.yml is committed to Git in plain text. Passwords, API keys, and connection strings belong in env_var() reading from actual environment variables, never in vars.
Forgetting the default causes a hard failure. Calling var("some_value") with no default and no declaration anywhere raises a compilation error the moment that model is parsed โ always provide a second-argument fallback for genuinely optional variables.
Overusing variables for things that should just be ref(). A variable that holds a hardcoded table name is usually a sign the underlying data should have been declared as a proper source or model instead.
Summary
| Element | Purpose |
|---|---|
vars: in dbt_project.yml | Declares project-wide default values |
var("name") | Reads a variable inside a model |
var("name", default) | Reads a variable with an inline fallback |
--vars '{"key": "value"}' | Overrides variables for a single CLI invocation |
var() turns one-off hardcoded values into declared, overridable parameters โ the mechanism that makes backfills, feature flags, and environment-specific behavior possible without forking your models.