dbt

dbt pre-hook / post-hook Deep Dive: Practical Patterns and Exam Prep

2026-04-19
NicheeLab Editorial Team

pre-hooks and post-hooks let you inject SQL or macros immediately before or after each node (model, snapshot, seed, etc.) runs. They are useful for automating grants, audit logging, statistics refreshes, and similar tasks.

This article assumes the stable behavior described in the official docs and lays out the precautions you need to use hooks safely in production, plus the comparison points that come up on the Analytics Engineer exam.

Core Concepts and Scope

A pre-hook is evaluated and executed immediately before its node runs; a post-hook runs immediately after. Hook bodies can be templated with Jinja and have access to context such as this, target, var, and env_var.

These are node-level hooks, distinct from on-run-start / on-run-end, which fire only once at the start or end of the entire project run.

  • Applies to: models, snapshots, and seeds (ephemeral models are excluded)
  • Execution unit: evaluated and run per target node (skipped nodes do not trigger hooks)
  • Defined in: a model file's config() block, or dbt_project.yml (+pre-hook/+post-hook)
  • Contents: raw SQL strings, Jinja expressions, or macro calls
  • Official: a stable feature documented in the dbt Docs Hooks page

Execution Order and Transaction Handling

Across a full dbt run, on-run-start fires once at the very beginning. Then, for each node, dbt runs pre-hook → main SQL → post-hook in order, and finally on-run-end fires once at the end.

On most adapters, the main SQL and the pre/post hooks share a single transaction. Setting transaction: false (on adapters that support it) runs the hook in its own transaction. Auto-commit behavior for DML/DDL is warehouse-specific, so it is safer to isolate things like grants and session settings when appropriate.

  • On pre-hook failure: the node is marked failed and the main SQL and post-hook are skipped
  • On post-hook failure: the node is marked failed, but the relation may already have been created or updated
  • Hooks defined in multiple scopes are merged and run in declaration order from top to bottom. Consolidate definitions in one place when order matters
  • Note that warehouses with auto-committed DDL (e.g. some Snowflake DDL) ignore transaction boundaries for those statements

Where hooks fit in the dbt run lifecycle

dbt runon-run-startmodel Apre-hook / SQL(main) / post-hookmodel Bpre-hook / SQL(main) / post-hookseed Cpre-hook / SQL(load) / post-hookon-run-endrun done

Representative Use Cases (Grants, Audit Logs, Session Settings)

Post-hooks are well suited for grants and statistics refreshes against a relation that was just created or replaced. Pre-hooks fit prep work such as cleaning staging areas or temporary tables before the main SQL runs.

For session- or query-level settings, combine hooks with sql_header and adapter-specific config (such as Snowflake's query_tag) for a more robust result.

  • Grants (post-hook): grant select on {{ this }} to role ...
  • Audit logging (pre/post): insert into an audit table to record model name, environment, and timestamp
  • Optimization and statistics (post): analyze/vacuum on Redshift, optimize/analyze on Databricks, etc.
  • Session settings (pre or sql_header): Snowflake query_tag, warehouse switching, etc.
  • Housekeeping (pre): cleaning up staging areas and work tables

Configuration and Authoring Patterns (dbt_project.yml / config() / Macros)

You can apply hooks broadly through dbt_project.yml model hierarchies or individually via a model's config() block. Multiple entries can be declared as a list and run top-to-bottom, and macro calls are allowed.

Where the adapter supports it, transaction: false isolates the hook in its own transaction. This is helpful for grants, logging, and other operations you do not want rolled back together with the main SQL on failure.

  • Bulk application: centralize grants and shared logging in dbt_project.yml
  • Per-model application: keep model-specific pre/post logic in the model's config()
  • Jinja: this, target, var, and env_var are available; use the log() macro for logging

Examples (dbt_project.yml, in-model config, and a macro)

# dbt_project.yml (excerpt)
models:
  my_project:
    +post-hook:
      - sql: grant select on {{ this }} to role ANALYST
        transaction: false
      - "{{ log('built ' ~ this, info=True) }}"

# models/fct_orders.sql (per-model hooks)
{{ config(
    materialized='table',
    pre_hook=[
      "delete from {{ ref('load_control') }} where model='{{ this.identifier }}'"
    ],
    post_hook=[
      "{{ insert_audit_record(this, 'fct_orders') }}"
    ]
) }}

select *
from {{ ref('stg_orders') }}

# macros/insert_audit_record.sql (audit-log macro)
{% macro insert_audit_record(relation, model_name) %}
  {% if execute %}
  insert into {{ ref('audit_log') }}(model, relation, executed_at, environment)
  values ('{{ model_name }}', '{{ relation }}', current_timestamp, '{{ target.name }}');
  {% endif %}
{% endmacro %}

Comparing pre / post / on-run and Exam Focus Points

On the Analytics Engineer exam, the typical talking points are hook granularity, execution timing, and transaction boundaries. Be ready to instantly pick the right hook for a given scenario.

In particular, memorize this split: on-run-start/end for once-per-run audit records, post-hook for grants applied to each model, and pre-hook for prep-time cleanup.

  • Use pre/post for node-level work and on-run for run-level work
  • Prefer the grants config when it is available; fall back to post-hook for more complex logic
  • Choose the transaction setting based on how you want rollback to behave on failure
Hook typeTiming / granularityTransactionPrimary use
pre-hookImmediately before each node runsUsually shares the main SQL's transaction; can be isolated where the adapter supports itPreparing temp tables, cleanup, setting session prerequisites
post-hookImmediately after each node runsUsually shares the main SQL's transaction; isolate to make grants and similar work saferGRANT, audit logs, statistics refreshes
on-run-start / endOnce at the start and end of the runRuns on its own, independent of any nodeRun-wide audit logging, environment setup and teardown

Pitfalls and Best Practices

Hooks are powerful, but mishandling transaction boundaries or ordering can lead to unintended side effects. The following guidelines keep things stable.

  • For grants, prefer dbt's grants config where supported and fall back to post-hook for complex role logic or unsupported adapters
  • Keep hook SQL idempotent (existence checks, create or replace, grants that tolerate re-execution)
  • When order matters, consolidate definitions in one place and avoid duplicating them in both the project and the model
  • For settings that should apply to the whole session, prefer sql_header or adapter-provided settings over pre-hook (which is per-node)
  • When writing to a shared audit table, design for key collisions and locking
  • Debug with dbt compile, --debug, log() output, and run-operation for isolated macro testing

Practice Question

Analytics Engineer

問題 1

A team wants to insert a single audit-log row at the start of every dbt run, and grant privileges immediately after each model is created or updated. Which configuration fits best?

  1. Use on-run-start for the audit log and a post-hook on each model for the GRANT
  2. Use a pre-hook on each model for the audit log and on-run-end for the GRANT
  3. Use on-run-start for the GRANT and a post-hook on each model for the audit log
  4. Use a pre-hook on each model to handle both the GRANT and the audit log

正解: A

Inserting a single audit row per run fits on-run-start. Granting privileges right after a model is created fits a per-node post-hook. A pre-hook runs before the main SQL, and GRANT statements belong on a relation that already exists.

Frequently Asked Questions

Do pre/post hooks work on ephemeral models?

No. Ephemeral models do not produce a physical relation, so pre/post hooks do not apply to them. Apply grants and other side effects on the downstream physical relations instead.

How do transactions behave, and what happens when a hook fails?

On most adapters, pre/post hooks run inside the same transaction as the main SQL. If a pre-hook fails, the main SQL never runs. A post-hook failure marks the node as failed, but the relation may already have been created or updated. Auto-committed DDL and other adapter-specific quirks exist, so separate hooks with transaction: false where needed and keep the SQL idempotent.

How should you debug and verify hooks?

Start with dbt compile to inspect the generated SQL, and use --debug for detailed logs. Macro-driven hooks can be tested in isolation via dbt run-operation. Emitting runtime info with the log() macro is also an effective verification technique.

Check what you learned with practice questions

Practice with certification-focused question sets

無料で問題を解いてみる
Author

NicheeLab Editorial Team

NicheeLab editorial team focused on data engineering and cloud certification learning. Content is structured around practical study needs and official exam domains.


Related articles
dbt

dbt Models: SQL-Defined Transformation Units (2026)

Model fundamentals — SELECT-based definitions, naming, refs,...

dbt

dbt Analytics Engineering Exam: Complete Guide (2026)

Pass the AE Certification — scope, weighting, sample questio...

dbt

dbt Cloud vs dbt Core: Feature & Cost Comparison (2026)

Honest comparison of dbt Cloud vs. dbt Core — IDE, scheduler...

dbt

dbt Project Structure: models/seeds/macros Layout (2026)

Recommended dbt project layout — models, seeds, macros, snap...

dbt

dbt_project.yml Explained: Every Config (2026)

Every dbt_project.yml setting that matters — paths, vars, ma...

Browse all dbt articles (101)
© 2026 NicheeLab All rights reserved.