dbt-expert — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited dbt-expert (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are working with dbt (data build tool) targeting Snowflake. Apply production-grade patterns at every level — clean SQL, thorough YAML metadata, sensible tests, and a project structure that scales.
| Type | When to use |
|---|---|
view | Lightweight lookups, rarely-queried transformations |
table | Stable staging/intermediate layers queried often |
incremental | Large fact tables — only process new/changed rows |
ephemeral | CTEs you want to reuse across models — no physical table |
snapshot | Slowly changing dimensions (SCD Type 2) |
dynamic_table | Snowflake-native: auto-refresh on a lag, no dbt run needed |
Configure at the model level (override project defaults):
{{ config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge',
cluster_by=['date_day', 'account_id']
) }}merge (default) — upsert by unique_key
{{ config(materialized='incremental', unique_key='id', incremental_strategy='merge') }}
select ...
from {{ source('raw', 'events') }}
{% if is_incremental() %}
where loaded_at > (select max(loaded_at) from {{ this }})
{% endif %}delete+insert — for partitioned tables without a reliable unique key
{{ config(
materialized='incremental',
unique_key=['date_day', 'account_id'],
incremental_strategy='delete+insert',
partition_by={'field': 'date_day', 'data_type': 'date'}
) }}append — event logs, audit tables where duplicates are acceptable
{{ config(materialized='incremental', incremental_strategy='append') }}insert_overwrite — replace entire partitions (Snowflake dynamic partition pruning)
On schema changes: always add on_schema_change='sync_all_columns' for production incremental models — silently ignoring schema drift causes silent data loss.
dbt_project/
├── dbt_project.yml # Project config
├── profiles.yml # Connection config (NOT in git)
├── packages.yml # dbt-utils, dbt-expectations, etc.
├── models/
│ ├── staging/ # 1:1 with source tables, light casting only
│ │ ├── _sources.yml # source() definitions + freshness
│ │ ├── _staging.yml # staging model docs + tests
│ │ └── stg_*.sql
│ ├── intermediate/ # Business logic joins, not exposed
│ │ └── int_*.sql
│ ├── marts/ # Final models consumed by BI/apps
│ │ ├── core/
│ │ └── finance/
│ └── utils/ # Generic macros used across layers
├── macros/
├── seeds/ # Small reference CSVs
├── snapshots/ # SCD Type 2 models
├── tests/ # Singular (custom SQL) tests
└── analyses/ # Ad hoc SQL, not materializedNaming rule: stg_<source>__<entity>, int_<entity>__<verb>, fct_<event>, dim_<entity>
# models/staging/_staging.yml
version: 2
models:
- name: stg_salesforce__accounts
description: "One row per Salesforce account, cast and renamed."
config:
tags: ["salesforce", "staging"]
columns:
- name: account_id
description: "Natural key from Salesforce."
data_tests:
- not_null
- unique
- name: created_at
description: "UTC timestamp of account creation."
data_tests:
- not_null
- name: account_type
data_tests:
- accepted_values:
values: ["Customer", "Partner", "Prospect"]# models/staging/_sources.yml
version: 2
sources:
- name: salesforce
database: RAW_DB
schema: SALESFORCE
freshness:
warn_after: {count: 12, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _loaded_at
tables:
- name: account
identifier: ACCOUNT # actual table name if different
description: "Raw Salesforce accounts."
columns:
- name: Id
data_tests:
- not_null
- uniqueGeneric tests (in schema.yml): not_null, unique, accepted_values, relationships
dbt-utils tests (install dbt-utils):
- dbt_utils.expression_is_true:
expression: "amount >= 0"
- dbt_utils.unique_combination_of_columns:
combination_of_columns: ["date_day", "account_id", "metric"]
- dbt_utils.not_empty_string:
column_name: account_namedbt-expectations for statistical tests:
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 1Singular tests (tests/ folder) — custom SQL that returns rows on failure:
-- tests/assert_fct_orders_amount_positive.sql
select order_id
from {{ ref('fct_orders') }}
where amount < 0Date spine:
-- macros/generate_date_spine.sql
{% macro generate_date_spine(start_date, end_date) %}
{{ dbt_utils.date_spine(
datepart="day",
start_date="cast('" ~ start_date ~ "' as date)",
end_date="cast('" ~ end_date ~ "' as date)"
) }}
{% endmacro %}Surrogate key:
select
{{ dbt_utils.generate_surrogate_key(['account_id', 'date_day']) }} as sk_account_day,
...Safe divide:
{% macro safe_divide(numerator, denominator) %}
iff({{ denominator }} = 0, null, {{ numerator }} / {{ denominator }})
{% endmacro %}-- snapshots/snp_accounts.sql
{% snapshot snp_accounts %}
{{
config(
target_schema='SNAPSHOTS',
unique_key='account_id',
strategy='timestamp',
updated_at='updated_at',
invalidate_hard_deletes=True
)
}}
select * from {{ source('crm', 'accounts') }}
{% endsnapshot %}Clustering keys (for large tables queried by date range):
{{ config(cluster_by=["date_day", "account_id"]) }}Dynamic tables (Snowflake-native, set materialized='dynamic_table'):
{{ config(
materialized='dynamic_table',
snowflake_warehouse='TRANSFORM_WH',
target_lag='5 minutes'
) }}Warehouse override for expensive models:
{{ config(snowflake_warehouse='LARGE_WH') }}Copy grants so downstream roles keep access after table rebuild:
{{ config(materialized='table', copy_grants=True) }}Stage-based seed loading for large reference files (avoid in-YAML seed if >10k rows):
-- Use a model that reads from a named Snowflake stage instead of dbt seed
select $1::varchar as code, $2::varchar as label
from @RAW_DB.PUBLIC.REFERENCE_STAGE/lookup.csv
(file_format => 'CSV_FORMAT')name: 'my_project'
version: '1.0.0'
config-version: 2
profile: 'snowflake_prod'
model-paths: ["models"]
test-paths: ["tests"]
snapshot-paths: ["snapshots"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
target-path: "target"
clean-targets: ["target", "dbt_packages"]
models:
my_project:
staging:
+materialized: view
+schema: STAGING
intermediate:
+materialized: ephemeral
marts:
+materialized: table
+schema: MARTS
core:
+tags: ["core", "daily"]
vars:
start_date: "2020-01-01"When generating dbt YAML files from code (e.g., from ETL metadata), follow these rules:
models/staging/_<layer>.ymlversion: 2 at the top of every YAML filedata_tests must match the SQL output column names exactlyidentifier field in sources is only needed when the dbt model name differs from the actual table nameref() creates lineage — always use it instead of hardcoding table namessource() for raw tables — never ref() a staging model that wraps a sourcedbt debug # Test connection
dbt compile --select my_model # See rendered SQL without running
dbt run --select +my_model # Run model + all ancestors
dbt test --select my_model # Run tests for one model
dbt docs generate && dbt docs serve # Browse lineage DAG
dbt source freshness # Check source staleness
dbt list --select tag:daily # Find all models tagged daily~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.