skip to content

How do you define a reusable macro in a dbt project and call it from a model?

level: juniorimportance: must knowfreq 66%

answer

  1. a named block of template text
  2. it lives in its own top-level folder
  3. curly braces at the call site
  4. macros/ directory and a macro/endmacro pair

basics

~20 s

Put a {% macro name(args) %} ... {% endmacro %} block in a .sql file under the macros/ directory, then call it from any model with {{ name(args) }}. The macro's rendered text is spliced into the model's SQL at compile time.

solid answer

~40 s

A dbt macro is a named Jinja block that returns text. You declare it with `{% macro cents_to_dollars(column_name, precision=2) %} ... {% endmacro %}` in a `.sql` file under `macros/`, and any model, test or hook in the project can call it as `{{ cents_to_dollars('amount_cents') }}`. The file name does not have to match the macro name and one file may hold several macros; dbt loads everything under the configured `macro-paths`. Arguments can have defaults, and `{{ return(value) }}` lets a macro hand back a real Python object such as a list rather than a string. A macro that does work outside a model — granting privileges, creating a UDF, a one-off cleanup — is invoked with `dbt run-operation my_macro --args '{key: value}'`.

code

sql · 4 lines
sql
-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, precision=2) %}
    round({{ column_name }} / 100.0, {{ precision }})
{% endmacro %}

go deeper

for a junior

Know the mechanics cold: a macro/endmacro block in a file under macros/, called with double curly braces, and remember that the result is just text pasted into your SQL.

for a middle

Explain argument defaults, return() for non-string values, and the fact that macro names are a flat project-wide namespace where duplicates are a parse error.

for a senior

Show judgment about what deserves a macro at all, and be able to script operational work with run-operation and a statement or run_query call inside the macro.

for a principal

Own the naming and ownership rules — a shared macro is an API across every model that calls it, so overrides of dbt internals and changes to widely-used helpers need review discipline.

## What a macro actually is A dbt macro is a Jinja macro: a named block of template text with parameters. Calling it renders its body with the arguments substituted, and the resulting text is pasted into whatever was being compiled. It is closer to a text function than to a database function — nothing about it exists in the warehouse, and the warehouse only ever sees the expanded result. ```sql -- macros/cents_to_dollars.sql {% macro cents_to_dollars(column_name, precision=2) %} round({{ column_name }} / 100.0, {{ precision }}) {% endmacro %} ``` ```sql -- models/stg_payments.sql select payment_id, {{ cents_to_dollars('amount_cents') }} as amount, {{ cents_to_dollars('fee_cents', 4) }} as fee from {{ source('stripe', 'payments') }} ``` Compiled, the second file contains `round(amount_cents / 100.0, 2) as amount` — ordinary SQL. ## Where the files live Macros go in `.sql` files under the `macros/` directory. The path is configurable through `macro-paths` in `dbt_project.yml`, but the default is what almost every project uses. Three details trip people up: - **The file name is irrelevant.** dbt indexes macros by macro name, not by file. `macros/utils.sql` may define five macros and all five are callable. - **Two macros with the same name in your project collide.** dbt will raise a duplicate-resource error at parse time; names are a flat namespace within a project. - **Macros are global to the project.** There is no import statement; once a macro is parsed, every model, test, hook and other macro can call it. ## Arguments, defaults and return values Parameters may have defaults (`precision=2` above), and callers may pass positionally or by keyword. Because a macro renders text, its value is a string by default. When you want a macro to hand back a real object — a list of column names, a boolean, a number to loop over — use `{{ return(...) }}`: ```sql {% macro active_regions() %} {{ return(['emea', 'amer', 'apac']) }} {% endmacro %} ``` Now `{% for region in active_regions() %}` iterates over a genuine list instead of trying to iterate the characters of a string. ## Calling a macro outside a model Some macros do not produce a fragment of a SELECT; they do an operational job. `dbt run-operation` executes such a macro on its own against the configured target: ```bash dbt run-operation grant_select --args '{role: reporter, schema: analytics}' ``` The macro body typically wraps its SQL in a `statement` block so it actually executes rather than being rendered into nowhere: ```sql {% macro grant_select(role, schema) %} {% set sql %} grant select on all tables in schema {{ schema }} to role {{ role }} {% endset %} {% do run_query(sql) %} {% endmacro %} ``` `{% do ... %}` evaluates an expression and discards its output, which is what you want when the point is the side effect. This is how teams script grants, warehouse maintenance and ad-hoc DDL without leaving the dbt project. ## Where else macros show up Hooks (`pre_hook`, `post_hook`, `on-run-end`) call macros, so a project can grant privileges after every build. Materializations themselves are macros, which is how custom materializations are written. And dbt's own behaviour is customisable by *overriding* specific built-in macros: defining `generate_schema_name` in your own `macros/` directory replaces dbt's default schema-naming logic project-wide. That override-by-name mechanism is powerful and easy to trip over — a macro named after a dbt internal is not a private helper. ## When a macro is the wrong tool A macro earns its place when the same SQL idiom appears in many models and would drift if copied: a currency conversion, a surrogate-key expression, a standard timestamp cast, a grant statement. It is the wrong tool when it hides the shape of a model — a macro that emits the whole FROM clause, or picks the grain, makes the model unreadable and the compiled output the only source of truth. Names matter for the same reason: `cents_to_dollars` reads at the call site, `fix_amount` does not. Finally, remember that macros are not tested by anything automatically. A macro used by forty models is a forty-model blast radius, so changes to one deserve the same care as a schema change.

  • How would you make a macro return a Python list rather than a string?
    Wrap the value in `{{ return([...]) }}` inside the macro body. Without it a macro always renders to text, so a caller that tries to loop over the result would iterate characters. `return()` is also how a macro hands back a boolean or a number that a caller wants to test rather than print.
  • What happens if you name a macro in your project the same as one of dbt's built-in macros?
    Your definition wins for the whole project. That is the supported way to customise behaviour such as `generate_schema_name` or `generate_alias_name`, but it means an accidentally-clashing helper name silently changes how dbt builds every model. Prefix internal helpers so you only override deliberately.
  • How do you run a macro that grants privileges without building any models?
    `dbt run-operation grant_select --args '{role: reporter}'`. dbt compiles just that macro against the target and executes whatever statements it issues, typically via `run_query` inside a `{% do %}` block. It is the standard way to script grants, maintenance DDL and one-off fixes from inside the project.

saying these in an interview costs you the question

  • Thinks a dbt macro creates a function in the warehouse
  • Believes the macro file name must match the macro name
  • Expects to import a macro before calling it
  • Calls a macro with {% %} instead of {{ }} and expects output
  • Puts macro files under models/ and wonders why nothing resolves

context