Data Engineering  /  dbt

🔄 dbt — Data Build Tool Guide 6 of 8 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.