Data Engineering  /  dbt

๐Ÿ”„ dbt โ€” Data Build Tool 60 guides ยท updated 2026

Analytics engineering with SQL โ€” models, tests, sources, and Jinja macros that turn raw warehouse tables into trustworthy, documented data products.

dbt run-operation: Executing Macros Directly From the CLI

Every macro covered in dbt Macros is designed to be called from inside a model or a hook, compiled as part of building something. But some operational tasks โ€” granting permissions across every table in a schema, cleaning up old partitions, generating a one-off report โ€” donโ€™t correspond to building a model at all. dbt run-operation exists for exactly this case: invoking a macro directly, independent of the model-build lifecycle.


Basic Usage

-- macros/grant_select_on_schema.sql
{% macro grant_select_on_schema(schema_name, role_name) %}
{% set relations = dbt_utils.get_relations_by_pattern(
schema_pattern=schema_name,
table_pattern='%'
) %}
{% for relation in relations %}
grant select on {{ relation }} to role {{ role_name }};
{% endfor %}
{% endmacro %}
Terminal window
dbt run-operation grant_select_on_schema --args '{"schema_name": "marts", "role_name": "analyst_role"}'

This runs the macro immediately, standalone โ€” no model needs to be built for this to execute, and the macro isnโ€™t tied to any single modelโ€™s lifecycle.


Passing Arguments

Arguments are passed as a YAML or JSON dictionary via --args, matching the macroโ€™s parameter names exactly.

Terminal window
dbt run-operation my_macro --args '{"start_date": "2026-01-01", "batch_size": 500}'
{% macro my_macro(start_date, batch_size) %}
{{ log("Processing from " ~ start_date ~ " with batch size " ~ batch_size, info=True) }}
{% endmacro %}

Note that arguments arrive as strings unless the macro explicitly casts them โ€” a common gotcha when a macro expects a number and receives the string "500" instead.


Common Real-World Uses

Bulk grants after a schema restructure. Rather than writing individual grant statements for dozens of tables, a macro can enumerate and grant across an entire schema in one command, as shown above.

Clearing old data from incremental models. A macro that deletes rows older than a retention window, run as a scheduled maintenance task independent of the regular model build:

{% macro purge_old_events(retention_days=90) %}
delete from {{ ref('stg_events') }}
where event_date < current_date - interval '{{ retention_days }} days'
{% endmacro %}
Terminal window
dbt run-operation purge_old_events --args '{"retention_days": 180}'

Generating a manifest of models matching a pattern, for use in external tooling or a report that doesnโ€™t fit dbtโ€™s normal build output.

Vacuuming or optimizing warehouse-specific maintenance operations that dbt doesnโ€™t have native support for, but that can be expressed as raw SQL wrapped in a macro.


run-operation vs. Hooks

Itโ€™s worth distinguishing this from the hooks covered in dbt Hooks. Hooks run automatically, tied to a modelโ€™s build or the overall invocation lifecycle (pre-hook, post-hook, on-run-start, on-run-end). run-operation runs only when explicitly invoked โ€” itโ€™s for tasks you trigger deliberately and separately, not ones that should happen every time a model builds.

Hooksrun-operation
TriggerAutomatic, tied to build lifecycleManual, explicit invocation
Typical useGrants that must happen every buildOne-off or scheduled-separately maintenance
Runs even if no models changeNoYes, independent of any build

Using run-operation in Scheduled Jobs

Maintenance macros invoked via run-operation are commonly scheduled separately from the main dbt build job, since theyโ€™re not tied to any specific modelโ€™s freshness.

# Example orchestration: weekly maintenance job, separate from the daily build
weekly_maintenance:
schedule: "0 2 * * 0"
commands:
- "dbt run-operation purge_old_events --args '{\"retention_days\": 365}'"

The generate_schema_name Macro: A Built-In run-operation Use Case

One of the most common reasons teams first encounter run-operation isnโ€™t a custom macro at all โ€” itโ€™s testing how dbtโ€™s built-in generate_schema_name macro (which controls schema naming across environments) will resolve, before running an actual build.

Terminal window
dbt run-operation generate_schema_name --args '{"custom_schema_name": "finance", "node": null}'

This lets you validate schema naming logic changes in isolation, without needing to build any actual models to see the result.


Debugging a run-operation Failure

Errors here look similar to any other Jinja/macro compilation error, but itโ€™s worth remembering thereโ€™s no โ€œmodelโ€ context to fall back on โ€” the error trace points directly at the macro itself.

Encountered an error while running operation: purge_old_events
Runtime Error
ref() is not allowed in run-operation macros unless in a model context

This particular error is common: some Jinja context (like ref() resolving to a compiled relation) behaves differently or isnโ€™t available at all outside a normal model compilation โ€” macros intended for run-operation should generally avoid relying on model-specific context and stick to explicit table/schema references instead.


Common Mistakes

Expecting run-operation to respect model selection syntax. It doesnโ€™t build or select models at all โ€” it invokes exactly one macro, once, with the arguments you provide.

Passing arguments with the wrong type. Arguments arrive from the CLI as strings by default; macros expecting numbers or booleans need to cast explicitly (| int, | bool Jinja filters) rather than assuming type coercion happens automatically.

Using run-operation for logic that should be a hook instead. If a macro genuinely needs to run on every single model build, a post-hook is the correct mechanism โ€” run-operation is for deliberate, separate invocations, not automatic ones.

Summary

ElementPurpose
dbt run-operation <macro>Invokes a macro directly, outside the model-build lifecycle
--argsPasses parameters to the macro as JSON/YAML
Typical usesBulk grants, maintenance cleanup, one-off administrative tasks
vs. hooksManual and independent, rather than automatic and build-tied

run-operation is the escape hatch for operational tasks that are real work your dbt project needs to do, but that donโ€™t correspond to building any particular table โ€” useful precisely because it doesnโ€™t try to force those tasks into the model lifecycle where they donโ€™t belong.