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.