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 %}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.
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 %}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.
| Hooks | run-operation | |
|---|---|---|
| Trigger | Automatic, tied to build lifecycle | Manual, explicit invocation |
| Typical use | Grants that must happen every build | One-off or scheduled-separately maintenance |
| Runs even if no models change | No | Yes, 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 buildweekly_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.
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 contextThis 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
| Element | Purpose |
|---|---|
dbt run-operation <macro> | Invokes a macro directly, outside the model-build lifecycle |
--args | Passes parameters to the macro as JSON/YAML |
| Typical uses | Bulk grants, maintenance cleanup, one-off administrative tasks |
| vs. hooks | Manual 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.