LFX Data Engineering
You are generating dbt models and SQL transformations that must be PR-ready. This skill encodes all conventions for the lf-dbt repository, which implements a medallion architecture data warehouse on Snowflake.
Prerequisites: Snowflake access must be provisioned first (via /lfx-snowflake-access).
Input Validation
Before generating any code, verify your args include:
| Required |
If Missing |
| Specific task (what to build/modify) |
Stop and ask — do not guess |
| Which medallion layer (bronze/silver/gold/platinum) |
Infer from task, but confirm |
| Data source name (for bronze) or upstream model (for silver+) |
Stop and ask — never assume |
| Target file path(s) |
Infer from naming conventions, but verify they exist |
| Example pattern to follow |
Find one yourself (see Read Before Generating) |
If invoked with a FIX: prefix, this is an error correction. Read the error, find the file, apply the targeted fix, and re-validate.
Read Before Generating — MANDATORY
Before writing ANY code, you MUST:
- Read the target file (if modifying) — understand what's already there
- Read one example file in the same layer and domain — match the exact patterns
- Read the relevant YML test file — ensure your model will be tested consistently
Do NOT generate code from memory alone. The codebase may have evolved since your training data.
# Example: before creating a new bronze model, read an existing one in the same source
cat models/bronze/fivetran_platform/bronze_fivetran_platform_events.sql
# And read the test file
cat models/bronze/fivetran_platform/bronze_fivetran_platform_tests.yml
License Header
Every new .sql file MUST start with this header:
-- Copyright The Linux Foundation and each contributor to LFX.
-- SPDX-License-Identifier: MIT
Every new .yml file MUST start with:
# Copyright The Linux Foundation and each contributor to LFX.
# SPDX-License-Identifier: MIT
Completion Report
When you finish, output a clear summary:
═══════════════════════════════════════════
/lfx-data-engineer COMPLETE
═══════════════════════════════════════════
Files created:
- models/bronze/fivetran_platform/bronze_fivetran_platform_new_table.sql
Files modified:
- models/bronze/fivetran_platform/bronze_fivetran_platform_tests.yml — added new_table tests
Validation:
- Ran: sqlfluff lint models/bronze/fivetran_platform/bronze_fivetran_platform_new_table.sql
- Result: ✓ passed / ✗ failed with: <error>
- Ran: dbt compile --select bronze_fivetran_platform_new_table
- Result: ✓ passed / ✗ failed with: <error>
Notes:
- Source table 'new_table' must exist in the fivetran_platform source definition
Errors:
- (none)
═══════════════════════════════════════════
Always include the Validation section. Run sqlfluff lint and dbt compile after creating or modifying files. Report the result.
Medallion Architecture Quick Reference
| Layer |
Materialization |
Schema |
Purpose |
| Bronze |
view (default) |
bronze_* (per source) |
1:1 with source data — column renames, type casting, filter deletes/test data |
| Silver |
table |
silver_dim, silver_fact |
Business logic, joins, reusable business objects |
| Gold |
table |
gold_* (per domain) |
Aggregated metrics for specific business use cases |
| Platinum |
table |
platinum* (per product) |
Pre-computed reports with time windows for dashboards |
References
| Task |
Reference |
| Environment setup, dbt commands, clone workflow |
references/getting-started.md |
| Detailed layer guide with SQL examples and decision tree |
references/medallion-architecture.md |
| SQL formatting, keyword casing, indentation, CTEs, JOINs |
references/sql-style-guide.md |
| dbt test conventions, PII tagging, primary key tests |
references/testing-patterns.md |
| Project macros: smart_source, format_timestamp, date ranges, deltas |
references/key-macros.md |
| Troubleshooting build failures, sqlfluff, incremental issues |
references/debugging-pipelines.md |
Creating a Model by Layer
Bronze — Source Extraction
Bronze models are 1:1 with source tables. They rename columns, cast types, and filter out deleted/test records. No business logic.
-- Copyright The Linux Foundation and each contributor to LFX.
-- SPDX-License-Identifier: MIT
SELECT
id AS event_id,
event_title AS event_name,
event_start_date,
event_end_date,
created_date AS event_created_ts,
lastmodified_date AS updated_at
FROM {{ source('fivetran_platform', 'event') }}
WHERE
NOT _fivetran_deleted
AND NOT is_test
Bronze rules:
- Use
source() to reference raw tables (or smart_source() for dev lookback)
- Rename columns to snake_case with business-friendly names
- Timestamps: suffix
_ts; Dates: suffix _date; Booleans: prefix is_ or has_
- Filter
_fivetran_deleted and test data rows
- No JOINs — one source table per model
- Use
get_warehouse('hourly') in config if the source is large
Silver — Business Logic
Silver models join bronze models, apply business rules, and create reusable objects. Split into dim/ (dimensions) and fact/ (facts).
-- Copyright The Linux Foundation and each contributor to LFX.
-- SPDX-License-Identifier: MIT
{% set warehouse = get_warehouse('hourly') %}
{{ config(snowflake_warehouse=warehouse) }}
/*
Purpose:
Create a reusable project dimension with core Salesforce project attributes
and the latest project health score for downstream analytics.
Questions answered:
- What are the canonical identifiers and names for each project?
- What is the current health score associated with each project?
Data sources:
- bronze_fivetran_salesforce_projects
- silver_fact_crowd_dev_project_health_metrics
*/
WITH source_data AS (
SELECT
project_id,
project_name,
project_slug,
project_status
FROM {{ ref('bronze_fivetran_salesforce_projects') }}
),
enriched AS (
SELECT
s.project_id,
s.project_name,
s.project_slug,
s.project_status,
h.health_score
FROM source_data s
LEFT JOIN {{ ref('silver_fact_crowd_dev_project_health_metrics') }} h
ON s.project_slug = h.project_slug
)
SELECT
project_id,
project_name,
project_slug,
project_status,
health_score
FROM enriched
Silver rules:
- Use
ref() to reference bronze or other silver models
- CTEs for each logical step (one unit of work per CTE)
- Verbose CTE names that describe what they do
- Include a block comment at the top explaining purpose, questions answered, and data sources
dim/ for slowly-changing attributes; fact/ for events and transactions
Gold — Aggregated Metrics
Gold models combine silver models into purpose-built datasets for specific use cases.
-- Copyright The Linux Foundation and each contributor to LFX.
-- SPDX-License-Identifier: MIT
{{ config(unique_key=["_key", "project_id"]) }}
SELECT
({{ dbt_utils.generate_surrogate_key(["c._key", "p.mapped_project_id"]) }}) AS activity_project_id,
c._key,
c.activity_id,
c.activity_ts,
p.mapped_project_id AS project_id,
p.mapped_project_slug AS project_slug
FROM {{ ref("silver_fact_crowd_dev_activities") }} c
LEFT JOIN {{ ref("_silver_dim_project_spine") }} p
ON c.project_id = p.base_project_id
WHERE
p.mapped_project_id IS NOT NULL
AND {{ filter_code_contributions_non_bot('c') }}
Gold rules:
- Use
dbt_utils.generate_surrogate_key() for composite primary keys
- Always specify
unique_key in config for incremental models
- Reference silver models via
ref(), apply domain-specific macros
- Final SELECT should explicitly list all columns — no
SELECT *
Platinum — Pre-Computed Reports
Platinum models produce dashboard-ready data with time-windowed aggregations.
-- Copyright The Linux Foundation and each contributor to LFX.
-- SPDX-License-Identifier: MIT
{% set warehouse = get_warehouse('hourly') %}
{{ config(snowflake_warehouse=warehouse) }}
WITH base AS (
SELECT
user_id,
event_id,
event_name,
event_start_date
FROM {{ ref('silver_fact_event_registrations') }}
WHERE event_name IS NOT NULL
)
SELECT
({{ dbt_utils.generate_surrogate_key(['user_id', 'event_id']) }}) AS _key,
user_id,
event_id,
event_name,
event_start_date
FROM base
QUALIFY ROW_NUMBER() OVER (
PARTITION BY user_id, event_id
ORDER BY event_start_date
) = 1
Platinum rules:
- Use date range macros (
is_last_30_days, is_year_to_date, etc.) for time windows
- Use
get_warehouse() for resource-intensive models
GROUP BY ALL is acceptable for complex aggregations
QUALIFY with ROW_NUMBER() for deduplication
- Purpose-built for specific dashboards (PCC, Individual Dashboard, Org Dashboard)
Writing Tests (YML)
Every model needs a corresponding entry in a *_tests.yml file. Use data_tests: (not the deprecated tests:). Parameterized tests require the arguments: wrapper.
# Copyright The Linux Foundation and each contributor to LFX.
# SPDX-License-Identifier: MIT
version: 2
models:
- name: my_new_model
description: "What this model contains and its purpose."
columns:
- name: _key
description: "The unique primary key for the table."
data_tests:
- unique
- not_null
- dbt_utils.not_empty_string
- name: status
description: "The current status."
data_type: string
data_tests:
- not_null
- accepted_values:
arguments:
values: ["active", "inactive", "pending"]
- name: project_id
description: "Foreign key to the projects dimension."
data_type: string
data_tests:
- not_null
- relationships:
arguments:
to: ref('silver_dim_projects')
field: project_id
- name: email
description: "User email address"
data_type: string
config:
meta:
contains_pii: true
data_retention: "undefined"
See references/testing-patterns.md for full conventions.
SQL Style Rules (Summary)
| Rule |
Example |
| Uppercase SQL keywords |
SELECT, FROM, WHERE, LEFT JOIN |
| Lowercase identifiers |
event_id, project_name |
| 4-space indentation |
Indent columns under SELECT, conditions under WHERE |
| Trailing commas |
event_id, (not , event_id) |
| CTEs over subqueries |
Use WITH ... AS (...) instead of nested SELECT |
Default to INNER JOIN |
Use LEFT JOIN only when right side may have no matches |
No RIGHT JOIN |
Rewrite as LEFT JOIN |
No SELECT DISTINCT |
Requires architect approval |
GROUP BY by number |
GROUP BY 1, 2 preferred over column names |
| Explicit column lists |
No SELECT * in final SELECT |
| Pre-filter in CTEs |
Complex filtering on joined tables belongs in a CTE before the join |
See references/sql-style-guide.md for full formatting rules.
Key Macros
| Macro |
Purpose |
When to Use |
smart_source() |
Dev-friendly source wrapper with lookback |
Bronze models reading from source tables |
format_timestamp() |
Generate UTC _ts and local _ts_local columns |
Bronze models normalizing timestamps |
to_utc_timestamp() |
Convert local timestamp to UTC with dynamic timezone |
When timezone is a column, not a constant |
get_warehouse() |
Select warehouse by size (default, hourly, medium) |
Large models needing specific compute |
generate_alias_name |
Strips schema prefix from table name (e.g., silver_dim_ → table name) |
Automatic — configured in macros |
is_last_7_days(), is_last_30_days(), etc. |
Date range filters for time windows |
Platinum models with pre-computed periods |
is_prev_7_days(), is_prev_30_days(), etc. |
Previous period for period-over-period comparison |
Delta/change calculations |
add_delta_columns() |
Generate _prev, _diff, _delta columns |
Period-over-period metric comparisons |
get_month(), get_quarter() |
Human-readable date labels |
Display-friendly date columns |
gdpr_filter_email() |
Exclude GDPR-suppressed emails |
Any model exposing email addresses |
filter_code_contributions_non_bot() |
Exclude bot code contributions |
Code contribution models |
format_country() |
Normalize country names to canonical values |
Models with user-entered country data |
comprehensive_email_filter() |
Validate email format + exclude test emails |
Email-based models |
See references/key-macros.md for full documentation and usage examples.
Data Governance
PII Hard Rules
The plugin-wide data-privacy rules live in
../lfx/references/data-privacy.md. Read
that doc before generating any of the following:
- dbt seeds, fixtures,
dbt show examples in docs, or unit-test rows —
never paste values copied from a production Snowflake query. Fabricate with
user-1@example.com, Test User, testuser01, reserved phone blocks, and
fixed UUIDs. Use dbt_utils.generate_surrogate_key on synthetic inputs, not
real primary keys.
- New models in any layer (bronze, silver, gold, platinum) that expose
email, name, phone, address, or linked pseudonyms (LFID, GitHub handle,
Discord ID) — the column MUST be PII-tagged
(
config.meta.contains_pii: true) and MUST include
config.meta.data_retention: "undefined" per the existing convention
documented in the PII Tagging section below and
references/testing-patterns.md (see
the "PII Tagging" section there and the checklist item at
references/testing-patterns.md:349-350). Do not invent an alternative
retention value and do not omit the field; the "undefined" placeholder
is the convention until the shared testing-patterns.md guidance is
updated. Any downstream dbt show or docs snippet MUST use the safe
alternatives from data-privacy.md.
- Logging or
RAISE/ASSERT output inside macros or Python models —
never emit raw PII. Use a correlator that does not locate an individual
record: dbt's invocation_id / run_started_at, the model name plus
source table name, batch identifier, warehouse query_tag, or an
aggregate grouping key (source-table name + time bucket). Do not use
activity UID, a specific row's surrogate key, or source_table + row number — those are joinable back to a specific person's activity record
and are therefore linked pseudonyms per the canonical PII taxonomy. If a
user-linked correlator is genuinely required, emit a service-specific
keyed-HMAC pseudonym per the canonical rule in
../lfx/references/data-privacy.md
("Logging exception"). Plain hashes such as sha256(email) are not
acceptable.
- Any code path that would persist a PII column into a schema where the
column is not part of the documented contract — stop and ask the user
first; the answer is usually "drop the column."
The three filters below have distinct, non-interchangeable roles. Pick the
right one for the layer and source:
gdpr_filter_email(email_field) / gdpr_filter_email_list(field, delim)
(see references/key-macros.md): these are
the GDPR enforcement macros. Any model that surfaces email addresses to
downstream consumers — bronze, silver, gold, or platinum — MUST filter
through the appropriate variant so GDPR-suppressed addresses are removed.
comprehensive_email_filter(email_field) (see
references/key-macros.md, "Email Validation
Macros"): a data-quality filter (format validation + test-address
exclusion). Apply it where you need clean, deliverable email addresses;
it is not a GDPR substitute.
WHERE NOT _fivetran_deleted (see
references/medallion-architecture.md,
Bronze Layer key patterns): bronze-layer-only, and only for sources
whose ingest sets the Fivetran soft-delete column. Silver/gold/platinum
and non-Fivetran sources do not have this column and MUST NOT reference
it.
PII Tagging
Columns containing personally identifiable information (names, emails, addresses, etc.) must be tagged in the YML file. Use config.meta — not top-level meta.
columns:
- name: email
description: "User email address"
config:
meta:
contains_pii: true
data_retention: "undefined"
Timestamp Normalization
All timestamps must be normalized to UTC in the bronze layer:
- Timestamps:
_ts suffix, stored as TIMESTAMP_NTZ in UTC
- Dates:
_date suffix, stored as DATE
- Use
format_timestamp() macro for consistent conversion
- Use
convert_timezone() for explicit timezone conversion
Primary Key Convention
- Use
_key suffix for primary key columns
- Always add unique, not_null, and not_empty_string tests
Common Anti-Patterns — DO NOT DO THESE
| Anti-Pattern |
Correct Pattern |
| Missing license header |
Always add -- Copyright The Linux Foundation... |
tests: in YML |
Use data_tests: (dbt v1.10.5+) |
meta: at top level in YML |
Nest under config: → meta: |
Missing arguments: on parameterized tests |
accepted_values: → arguments: → values: |
tags: at top level in YML |
Nest under config: → tags: |
Duplicate config: keys in YML |
Combine into a single config: block |
Custom keys directly in config: |
Nest under config: → meta: |
SELECT DISTINCT |
Use GROUP BY or QUALIFY ROW_NUMBER() |
RIGHT JOIN |
Rewrite as LEFT JOIN |
Filtering right side of LEFT JOIN in WHERE |
Filter in the ON clause or in a CTE |
SELECT * in final select |
Explicitly list all columns |
Subqueries in FROM or JOIN |
Use CTEs |
Raw source() in dev (large tables) |
Use smart_source() with lookback |
| Hardcoded warehouse name |
Use get_warehouse() macro |
console.log / print debugging |
Use dbt compile and dbt show |
Committing without --signoff or -S |
Always use signed commits with DCO |
Pre-PR Checklist
All Models
Bronze Models
Silver Models
Gold Models
Platinum Models
Scope Boundaries
This skill DOES:
- Generate/modify dbt SQL models following medallion architecture
- Create/update YML test files with proper data_tests format
- Add source definitions for new data sources
- Apply project macros (smart_source, format_timestamp, date ranges, etc.)
- Run sqlfluff lint/fix validation after changes
- Run dbt compile to verify model correctness
This skill does NOT:
- Run dbt build/test against the warehouse (use the
running-dbt-commands skill)
- Modify existing macros without architect review
- Make architectural decisions about layer placement (ask the user)
- Generate semantic layer definitions (use the
building-dbt-semantic-layer skill)
- Troubleshoot dbt Cloud job failures (use the
troubleshooting-dbt-job-errors skill)
- Modify protected infrastructure files (
dbt_project.yml, profiles.yml, packages.yml) — flag for code owner
1---2name: lfx-data-engineer3description: Guide non-dbt developers through building PR-ready data models, tests, and transformations in the lf-dbt repo. Encodes the medallion architecture (bronze/silver/gold/platinum), Snowflake SQL conventions, sqlfluff formatting, dbt testing patterns, key macros, and data governance rules. Use this skill any time someone asks about writing dbt models, adding data tests, creating SQL transformations, fixing pipeline failures, or contributing to the lf-dbt repository.4---56<!-- Copyright The Linux Foundation and each contributor to LFX. -->7<!-- SPDX-License-Identifier: MIT -->8<!-- Tool names in this file use Claude Code vocabulary. See docs/tool-mapping.md for other platforms. -->910# LFX Data Engineering1112You are generating dbt models and SQL transformations that must be PR-ready. This skill encodes all conventions for the `lf-dbt` repository, which implements a medallion architecture data warehouse on Snowflake.1314**Prerequisites:** Snowflake access must be provisioned first (via `/lfx-snowflake-access`).1516## Input Validation1718Before generating any code, verify your args include:1920| Required | If Missing |21|----------|------------|22| Specific task (what to build/modify) | Stop and ask — do not guess |23| Which medallion layer (bronze/silver/gold/platinum) | Infer from task, but confirm |24| Data source name (for bronze) or upstream model (for silver+) | Stop and ask — never assume |25| Target file path(s) | Infer from naming conventions, but verify they exist |26| Example pattern to follow | Find one yourself (see Read Before Generating) |2728**If invoked with a FIX: prefix**, this is an error correction. Read the error, find the file, apply the targeted fix, and re-validate.2930## Read Before Generating — MANDATORY3132Before writing ANY code, you MUST:33341. **Read the target file** (if modifying) — understand what's already there352. **Read one example file** in the same layer and domain — match the exact patterns363. **Read the relevant YML test file** — ensure your model will be tested consistently3738Do NOT generate code from memory alone. The codebase may have evolved since your training data.3940```bash41# Example: before creating a new bronze model, read an existing one in the same source42cat models/bronze/fivetran_platform/bronze_fivetran_platform_events.sql43# And read the test file44cat models/bronze/fivetran_platform/bronze_fivetran_platform_tests.yml45```4647## License Header4849Every new `.sql` file MUST start with this header:5051```sql52-- Copyright The Linux Foundation and each contributor to LFX.53-- SPDX-License-Identifier: MIT54```5556Every new `.yml` file MUST start with:5758```yaml59# Copyright The Linux Foundation and each contributor to LFX.60# SPDX-License-Identifier: MIT61```6263## Completion Report6465When you finish, output a clear summary:6667```68═══════════════════════════════════════════69/lfx-data-engineer COMPLETE70═══════════════════════════════════════════71Files created:72 - models/bronze/fivetran_platform/bronze_fivetran_platform_new_table.sql7374Files modified:75 - models/bronze/fivetran_platform/bronze_fivetran_platform_tests.yml — added new_table tests7677Validation:78 - Ran: sqlfluff lint models/bronze/fivetran_platform/bronze_fivetran_platform_new_table.sql79 - Result: ✓ passed / ✗ failed with: <error>80 - Ran: dbt compile --select bronze_fivetran_platform_new_table81 - Result: ✓ passed / ✗ failed with: <error>8283Notes:84 - Source table 'new_table' must exist in the fivetran_platform source definition8586Errors:87 - (none)88═══════════════════════════════════════════89```9091**Always include the Validation section.** Run `sqlfluff lint` and `dbt compile` after creating or modifying files. Report the result.9293---9495## Medallion Architecture Quick Reference9697| Layer | Materialization | Schema | Purpose |98|-------|----------------|--------|---------|99| **Bronze** | `view` (default) | `bronze_*` (per source) | 1:1 with source data — column renames, type casting, filter deletes/test data |100| **Silver** | `table` | `silver_dim`, `silver_fact` | Business logic, joins, reusable business objects |101| **Gold** | `table` | `gold_*` (per domain) | Aggregated metrics for specific business use cases |102| **Platinum** | `table` | `platinum*` (per product) | Pre-computed reports with time windows for dashboards |103104### References105106| Task | Reference |107|------|-----------|108| Environment setup, dbt commands, clone workflow | [references/getting-started.md](references/getting-started.md) |109| Detailed layer guide with SQL examples and decision tree | [references/medallion-architecture.md](references/medallion-architecture.md) |110| SQL formatting, keyword casing, indentation, CTEs, JOINs | [references/sql-style-guide.md](references/sql-style-guide.md) |111| dbt test conventions, PII tagging, primary key tests | [references/testing-patterns.md](references/testing-patterns.md) |112| Project macros: smart_source, format_timestamp, date ranges, deltas | [references/key-macros.md](references/key-macros.md) |113| Troubleshooting build failures, sqlfluff, incremental issues | [references/debugging-pipelines.md](references/debugging-pipelines.md) |114115---116117## Creating a Model by Layer118119### Bronze — Source Extraction120121Bronze models are 1:1 with source tables. They rename columns, cast types, and filter out deleted/test records. No business logic.122123```sql124-- Copyright The Linux Foundation and each contributor to LFX.125-- SPDX-License-Identifier: MIT126127SELECT128 id AS event_id,129 event_title AS event_name,130 event_start_date,131 event_end_date,132 created_date AS event_created_ts,133 lastmodified_date AS updated_at134135FROM {{ source('fivetran_platform', 'event') }}136WHERE137 NOT _fivetran_deleted138 AND NOT is_test139```140141**Bronze rules:**142- Use `source()` to reference raw tables (or `smart_source()` for dev lookback)143- Rename columns to snake_case with business-friendly names144- Timestamps: suffix `_ts`; Dates: suffix `_date`; Booleans: prefix `is_` or `has_`145- Filter `_fivetran_deleted` and test data rows146- No JOINs — one source table per model147- Use `get_warehouse('hourly')` in config if the source is large148149### Silver — Business Logic150151Silver models join bronze models, apply business rules, and create reusable objects. Split into `dim/` (dimensions) and `fact/` (facts).152153```sql154-- Copyright The Linux Foundation and each contributor to LFX.155-- SPDX-License-Identifier: MIT156157{% set warehouse = get_warehouse('hourly') %}158159{{ config(snowflake_warehouse=warehouse) }}160161/*162Purpose:163 Create a reusable project dimension with core Salesforce project attributes164 and the latest project health score for downstream analytics.165166Questions answered:167 - What are the canonical identifiers and names for each project?168 - What is the current health score associated with each project?169170Data sources:171 - bronze_fivetran_salesforce_projects172 - silver_fact_crowd_dev_project_health_metrics173*/174175WITH source_data AS (176 SELECT177 project_id,178 project_name,179 project_slug,180 project_status181 FROM {{ ref('bronze_fivetran_salesforce_projects') }}182),183184enriched AS (185 SELECT186 s.project_id,187 s.project_name,188 s.project_slug,189 s.project_status,190 h.health_score191 FROM source_data s192 LEFT JOIN {{ ref('silver_fact_crowd_dev_project_health_metrics') }} h193 ON s.project_slug = h.project_slug194)195196SELECT197 project_id,198 project_name,199 project_slug,200 project_status,201 health_score202FROM enriched203```204205**Silver rules:**206- Use `ref()` to reference bronze or other silver models207- CTEs for each logical step (one unit of work per CTE)208- Verbose CTE names that describe what they do209- Include a block comment at the top explaining purpose, questions answered, and data sources210- `dim/` for slowly-changing attributes; `fact/` for events and transactions211212### Gold — Aggregated Metrics213214Gold models combine silver models into purpose-built datasets for specific use cases.215216```sql217-- Copyright The Linux Foundation and each contributor to LFX.218-- SPDX-License-Identifier: MIT219220{{ config(unique_key=["_key", "project_id"]) }}221222SELECT223 ({{ dbt_utils.generate_surrogate_key(["c._key", "p.mapped_project_id"]) }}) AS activity_project_id,224 c._key,225 c.activity_id,226 c.activity_ts,227 p.mapped_project_id AS project_id,228 p.mapped_project_slug AS project_slug229230FROM {{ ref("silver_fact_crowd_dev_activities") }} c231LEFT JOIN {{ ref("_silver_dim_project_spine") }} p232 ON c.project_id = p.base_project_id233WHERE234 p.mapped_project_id IS NOT NULL235 AND {{ filter_code_contributions_non_bot('c') }}236```237238**Gold rules:**239- Use `dbt_utils.generate_surrogate_key()` for composite primary keys240- Always specify `unique_key` in config for incremental models241- Reference silver models via `ref()`, apply domain-specific macros242- Final SELECT should explicitly list all columns — no `SELECT *`243244### Platinum — Pre-Computed Reports245246Platinum models produce dashboard-ready data with time-windowed aggregations.247248```sql249-- Copyright The Linux Foundation and each contributor to LFX.250-- SPDX-License-Identifier: MIT251252{% set warehouse = get_warehouse('hourly') %}253254{{ config(snowflake_warehouse=warehouse) }}255256WITH base AS (257 SELECT258 user_id,259 event_id,260 event_name,261 event_start_date262 FROM {{ ref('silver_fact_event_registrations') }}263 WHERE event_name IS NOT NULL264)265266SELECT267 ({{ dbt_utils.generate_surrogate_key(['user_id', 'event_id']) }}) AS _key,268 user_id,269 event_id,270 event_name,271 event_start_date272FROM base273QUALIFY ROW_NUMBER() OVER (274 PARTITION BY user_id, event_id275 ORDER BY event_start_date276) = 1277```278279**Platinum rules:**280- Use date range macros (`is_last_30_days`, `is_year_to_date`, etc.) for time windows281- Use `get_warehouse()` for resource-intensive models282- `GROUP BY ALL` is acceptable for complex aggregations283- `QUALIFY` with `ROW_NUMBER()` for deduplication284- Purpose-built for specific dashboards (PCC, Individual Dashboard, Org Dashboard)285286---287288## Writing Tests (YML)289290Every model needs a corresponding entry in a `*_tests.yml` file. Use `data_tests:` (not the deprecated `tests:`). Parameterized tests require the `arguments:` wrapper.291292```yaml293# Copyright The Linux Foundation and each contributor to LFX.294# SPDX-License-Identifier: MIT295296version: 2297models:298 - name: my_new_model299 description: "What this model contains and its purpose."300 columns:301 - name: _key302 description: "The unique primary key for the table."303 data_tests:304 - unique305 - not_null306 - dbt_utils.not_empty_string307308 - name: status309 description: "The current status."310 data_type: string311 data_tests:312 - not_null313 - accepted_values:314 arguments:315 values: ["active", "inactive", "pending"]316317 - name: project_id318 description: "Foreign key to the projects dimension."319 data_type: string320 data_tests:321 - not_null322 - relationships:323 arguments:324 to: ref('silver_dim_projects')325 field: project_id326327 - name: email328 description: "User email address"329 data_type: string330 config:331 meta:332 contains_pii: true333 data_retention: "undefined"334```335336See [references/testing-patterns.md](references/testing-patterns.md) for full conventions.337338---339340## SQL Style Rules (Summary)341342| Rule | Example |343|------|---------|344| Uppercase SQL keywords | `SELECT`, `FROM`, `WHERE`, `LEFT JOIN` |345| Lowercase identifiers | `event_id`, `project_name` |346| 4-space indentation | Indent columns under `SELECT`, conditions under `WHERE` |347| Trailing commas | `event_id,` (not `, event_id`) |348| CTEs over subqueries | Use `WITH ... AS (...)` instead of nested `SELECT` |349| Default to `INNER JOIN` | Use `LEFT JOIN` only when right side may have no matches |350| No `RIGHT JOIN` | Rewrite as `LEFT JOIN` |351| No `SELECT DISTINCT` | Requires architect approval |352| `GROUP BY` by number | `GROUP BY 1, 2` preferred over column names |353| Explicit column lists | No `SELECT *` in final SELECT |354| Pre-filter in CTEs | Complex filtering on joined tables belongs in a CTE before the join |355356See [references/sql-style-guide.md](references/sql-style-guide.md) for full formatting rules.357358---359360## Key Macros361362| Macro | Purpose | When to Use |363|-------|---------|-------------|364| `smart_source()` | Dev-friendly source wrapper with lookback | Bronze models reading from source tables |365| `format_timestamp()` | Generate UTC `_ts` and local `_ts_local` columns | Bronze models normalizing timestamps |366| `to_utc_timestamp()` | Convert local timestamp to UTC with dynamic timezone | When timezone is a column, not a constant |367| `get_warehouse()` | Select warehouse by size (`default`, `hourly`, `medium`) | Large models needing specific compute |368| `generate_alias_name` | Strips schema prefix from table name (e.g., `silver_dim_` → table name) | Automatic — configured in macros |369| `is_last_7_days()`, `is_last_30_days()`, etc. | Date range filters for time windows | Platinum models with pre-computed periods |370| `is_prev_7_days()`, `is_prev_30_days()`, etc. | Previous period for period-over-period comparison | Delta/change calculations |371| `add_delta_columns()` | Generate `_prev`, `_diff`, `_delta` columns | Period-over-period metric comparisons |372| `get_month()`, `get_quarter()` | Human-readable date labels | Display-friendly date columns |373| `gdpr_filter_email()` | Exclude GDPR-suppressed emails | Any model exposing email addresses |374| `filter_code_contributions_non_bot()` | Exclude bot code contributions | Code contribution models |375| `format_country()` | Normalize country names to canonical values | Models with user-entered country data |376| `comprehensive_email_filter()` | Validate email format + exclude test emails | Email-based models |377378See [references/key-macros.md](references/key-macros.md) for full documentation and usage examples.379380---381382## Data Governance383384### PII Hard Rules385386The plugin-wide data-privacy rules live in387[`../lfx/references/data-privacy.md`](../lfx/references/data-privacy.md). Read388that doc before generating any of the following:389390- **dbt seeds, fixtures, `dbt show` examples in docs, or unit-test rows** —391 never paste values copied from a production Snowflake query. Fabricate with392 `user-1@example.com`, `Test User`, `testuser01`, reserved phone blocks, and393 fixed UUIDs. Use `dbt_utils.generate_surrogate_key` on synthetic inputs, not394 real primary keys.395- **New models in any layer (bronze, silver, gold, platinum) that expose396 email, name, phone, address, or linked pseudonyms (LFID, GitHub handle,397 Discord ID)** — the column MUST be PII-tagged398 (`config.meta.contains_pii: true`) and MUST include399 `config.meta.data_retention: "undefined"` per the existing convention400 documented in the *PII Tagging* section below and401 [`references/testing-patterns.md`](references/testing-patterns.md) (see402 the "PII Tagging" section there and the checklist item at403 `references/testing-patterns.md:349-350`). Do not invent an alternative404 retention value and do not omit the field; the `"undefined"` placeholder405 is the convention until the shared testing-patterns.md guidance is406 updated. Any downstream `dbt show` or docs snippet MUST use the safe407 alternatives from `data-privacy.md`.408- **Logging or `RAISE`/`ASSERT` output inside macros or Python models** —409 never emit raw PII. Use a correlator that does not locate an individual410 record: dbt's `invocation_id` / `run_started_at`, the model name plus411 source table name, batch identifier, warehouse `query_tag`, or an412 aggregate grouping key (source-table name + time bucket). Do **not** use413 `activity UID`, a specific row's surrogate key, or `source_table + row414 number` — those are joinable back to a specific person's activity record415 and are therefore linked pseudonyms per the canonical PII taxonomy. If a416 user-linked correlator is genuinely required, emit a service-specific417 **keyed-HMAC pseudonym** per the canonical rule in418 [`../lfx/references/data-privacy.md`](../lfx/references/data-privacy.md)419 ("Logging exception"). Plain hashes such as `sha256(email)` are not420 acceptable.421- **Any code path that would persist a PII column into a schema where the422 column is not part of the documented contract** — stop and ask the user423 first; the answer is usually "drop the column."424425The three filters below have distinct, non-interchangeable roles. Pick the426right one for the layer and source:427428- **`gdpr_filter_email(email_field)` / `gdpr_filter_email_list(field, delim)`**429 (see [`references/key-macros.md`](references/key-macros.md)): these are430 the GDPR enforcement macros. Any model that surfaces email addresses to431 downstream consumers — bronze, silver, gold, or platinum — MUST filter432 through the appropriate variant so GDPR-suppressed addresses are removed.433- **`comprehensive_email_filter(email_field)`** (see434 [`references/key-macros.md`](references/key-macros.md), "Email Validation435 Macros"): a data-quality filter (format validation + test-address436 exclusion). Apply it where you need clean, deliverable email addresses;437 it is not a GDPR substitute.438- **`WHERE NOT _fivetran_deleted`** (see439 [`references/medallion-architecture.md`](references/medallion-architecture.md),440 Bronze Layer key patterns): bronze-layer-only, and only for sources441 whose ingest sets the Fivetran soft-delete column. Silver/gold/platinum442 and non-Fivetran sources do not have this column and MUST NOT reference443 it.444445### PII Tagging446447Columns containing personally identifiable information (names, emails, addresses, etc.) must be tagged in the YML file. Use `config.meta` — not top-level `meta`.448449```yaml450columns:451 - name: email452 description: "User email address"453 config:454 meta:455 contains_pii: true456 data_retention: "undefined"457```458459### Timestamp Normalization460461All timestamps must be normalized to UTC in the bronze layer:462- Timestamps: `_ts` suffix, stored as `TIMESTAMP_NTZ` in UTC463- Dates: `_date` suffix, stored as `DATE`464- Use `format_timestamp()` macro for consistent conversion465- Use `convert_timezone()` for explicit timezone conversion466467### Primary Key Convention468469- Use `_key` suffix for primary key columns470- Always add unique, not_null, and not_empty_string tests471472---473474## Common Anti-Patterns — DO NOT DO THESE475476| Anti-Pattern | Correct Pattern |477|-------------|-----------------|478| Missing license header | Always add `-- Copyright The Linux Foundation...` |479| `tests:` in YML | Use `data_tests:` (dbt v1.10.5+) |480| `meta:` at top level in YML | Nest under `config:` → `meta:` |481| Missing `arguments:` on parameterized tests | `accepted_values:` → `arguments:` → `values:` |482| `tags:` at top level in YML | Nest under `config:` → `tags:` |483| Duplicate `config:` keys in YML | Combine into a single `config:` block |484| Custom keys directly in `config:` | Nest under `config:` → `meta:` |485| `SELECT DISTINCT` | Use `GROUP BY` or `QUALIFY ROW_NUMBER()` |486| `RIGHT JOIN` | Rewrite as `LEFT JOIN` |487| Filtering right side of LEFT JOIN in `WHERE` | Filter in the `ON` clause or in a CTE |488| `SELECT *` in final select | Explicitly list all columns |489| Subqueries in `FROM` or `JOIN` | Use CTEs |490| Raw `source()` in dev (large tables) | Use `smart_source()` with lookback |491| Hardcoded warehouse name | Use `get_warehouse()` macro |492| `console.log` / `print` debugging | Use `dbt compile` and `dbt show` |493| Committing without `--signoff` or `-S` | Always use signed commits with DCO |494495---496497## Pre-PR Checklist498499### All Models500- [ ] License header on all new `.sql` and `.yml` files501- [ ] Model documented in corresponding `*_tests.yml` file502- [ ] Primary key column(s) have `unique`, `not_null`, `dbt_utils.not_empty_string` tests503- [ ] PII columns tagged with `config.meta.contains_pii: true` and `data_retention: "undefined"`504- [ ] `sqlfluff lint` passes on all new/modified `.sql` files505- [ ] `dbt compile --select +model_name` succeeds506- [ ] Column naming follows conventions (`_ts`, `_date`, `is_`, `has_`, `_key`)507- [ ] No `SELECT *` in final select statements508- [ ] All timestamps normalized to UTC509510### Bronze Models511- [ ] 1:1 with source table — no joins512- [ ] Filters `_fivetran_deleted` and test data513- [ ] Column renames to snake_case with business-friendly names514- [ ] Uses `source()` or `smart_source()`515516### Silver Models517- [ ] Uses `ref()` to reference upstream models518- [ ] CTEs for each logical unit of work519- [ ] Block comment explaining purpose and data sources520- [ ] Placed in correct subfolder (`dim/` or `fact/`)521522### Gold Models523- [ ] Surrogate key generated for composite keys524- [ ] `unique_key` specified in config for incremental models525- [ ] Final SELECT explicitly lists all columns526527### Platinum Models528- [ ] Uses date range macros for time windows529- [ ] `get_warehouse()` configured if resource-intensive530- [ ] Purpose-built for a specific dashboard or use case531532---533534## Scope Boundaries535536**This skill DOES:**537- Generate/modify dbt SQL models following medallion architecture538- Create/update YML test files with proper data_tests format539- Add source definitions for new data sources540- Apply project macros (smart_source, format_timestamp, date ranges, etc.)541- Run sqlfluff lint/fix validation after changes542- Run dbt compile to verify model correctness543544**This skill does NOT:**545- Run dbt build/test against the warehouse (use the `running-dbt-commands` skill)546- Modify existing macros without architect review547- Make architectural decisions about layer placement (ask the user)548- Generate semantic layer definitions (use the `building-dbt-semantic-layer` skill)549- Troubleshoot dbt Cloud job failures (use the `troubleshooting-dbt-job-errors` skill)550- Modify protected infrastructure files (`dbt_project.yml`, `profiles.yml`, `packages.yml`) — flag for code owner