Executive Summary
Built AT&T Marketing Operations' Business Intelligence platform, consolidating data from Workfront (project management), Salesforce (CRM), and ZoomInfo (contact data) via Fivetran into Snowflake and powering self-service analytics for 150+ users. Designed and built 25+ Power BI dashboards for executive leadership tracking campaign performance, workforce costs, budget allocations, and operational KPIs. Developed DAX measures for YoY growth, rolling averages, cohort analysis, and variance-to-budget calculations. Reduced dashboard query times from 45 seconds to 3 seconds (93% improvement) through optimized dimensional models. Reduced IT data request tickets by 60% and manual data retrieval by 40%.
The Problem Statement: When I joined AT&T's Marketing Technology team in March 2024, the analytics workflow was entirely manual. Campaign managers exported data from Workfront, pasted it into Excel, manually joined it with Salesforce exports, and produced reports that were outdated by the time they were distributed. 200+ concurrent campaigns, zero single source of truth.
The Decision: Rather than building another one-off dashboard, I proposed a proper BI platform: a unified Snowflake reporting layer fed by Fivetran, with a Power BI semantic layer on top enabling self-service analytics. The key question was always "who is consuming this data and what decisions are they making?" — every dimension, every measure, and every dashboard layout was designed with the business user in mind first.
The Result: 12 months later: 25+ Power BI dashboards in production, 150+ self-service users, and the analytics team no longer spends 40% of their time pulling data manually.
Key Outcomes
- 25+ Power BI dashboards deployed to production for executive leadership
- 150+ self-service users on Power BI — up from a team of 3 analysts
- 60% reduction in IT data request tickets
- 40% reduction in manual data retrieval across marketing operations
- 45s → 3s Power BI query latency improvement (query optimization)
- 200+ concurrent campaigns tracked via incremental dbt models
BI Platform Architecture
Workfront
Project Tasks
Timelines
Resource Plans
Salesforce
CRM Contacts
Opportunities
Account Data
ZoomInfo
Company Data
Contact Enrichment
Intent Signals
Fivetran → Snowflake → dbt
Analytics Engineering Platform
Fivetran syncs all three sources into Snowflake raw schemas on a scheduled cadence. dbt staging models clean and normalize the raw data; intermediate models apply business logic (campaign attribution, SCD Type 2 organizational tracking, date spine joins); mart models produce the final star schema consumed by Power BI.
Key Performance Indicators
Technical Deep Dive
Dimensional Data Model Architecture
Before: Manual Process
- Workfront CSV exports → manual Excel joins
- Salesforce reports downloaded ad hoc
- No version control, no lineage, no tests
- 3 analysts spending 40% of time on data prep
- Power BI queries timing out at 45+ seconds
- Each team had its own definition of "active campaign"
After: dbt + Snowflake Platform
- Fivetran → Snowflake raw → dbt staging models
- Intermediate models: business logic, SCD Type 2, attribution
- Mart models: star schema facts + conformed dimensions
- 150+ users consuming self-service Power BI
- Sub-3-second query performance via Snowflake clustering
- Single conformed definition for every business concept
Incremental Models for Campaign Data
With 200+ concurrent campaigns producing daily volume metrics, full-refresh models were impractical. Each incremental dbt model uses a unique_key composite (campaign_id + date) and a max(updated_at) filter → meaning only new or changed records are processed per run. This pattern scaled campaign fact tables to handle daily Workfront sync without warehouse timeouts.
dbt Incremental Model → fct_campaign_daily.sql:
-- models/marts/marketing/fct_campaign_daily.sql
-- Incremental model: processes only new/updated campaign records each run
-- Grain: one row per campaign per day
{{
config(
materialized='incremental',
unique_key='campaign_day_key',
on_schema_change='sync_all_columns',
cluster_by=['campaign_date', 'department_key']
)
}}
with workfront_tasks as (
select * from {{ ref('int_workfront_tasks_normalized') }}
{% if is_incremental() %}
where updated_at > (select max(loaded_at) from {{ this }})
{% endif %}
),
salesforce_campaigns as (
select * from {{ ref('int_salesforce_campaigns_active') }}
),
date_spine as (
-- reusable macro: generates daily date records for a rolling window
{{ dbt_utils.date_spine(
datepart = "day",
start_date = "cast('2024-01-01' as date)",
end_date = "current_date()"
) }}
),
campaign_daily as (
select
{{ dbt_utils.generate_surrogate_key([
'wt.campaign_id', 'ds.date_day'
]) }} as campaign_day_key,
ds.date_day as campaign_date,
wt.campaign_id,
wt.campaign_name,
wt.department_key,
wt.owner_employee_key,
sc.campaign_type,
sc.budget_amount,
-- calculated daily metrics
sum(wt.planned_hours) as planned_hours_day,
sum(wt.actual_hours) as actual_hours_day,
count(distinct wt.task_id) as task_count,
count(distinct case when wt.status = 'COMPLETE'
then wt.task_id end) as tasks_completed,
-- budget variance
coalesce(sc.budget_amount, 0) -
coalesce(sc.actual_spend_to_date, 0) as budget_variance,
current_timestamp() as loaded_at
from date_spine ds
join workfront_tasks wt
on ds.date_day between wt.planned_start_date and wt.planned_end_date
left join salesforce_campaigns sc
on wt.campaign_id = sc.campaign_id
group by 1,2,3,4,5,6,7,8
)
select * from campaign_daily
SCD Type 2 → Organizational Change Tracking
AT&T underwent multiple reorganizations during 2024. Department hierarchies, team assignments, and campaign ownership changed frequently. Rather than overwrite historical records, I implemented SCD Type 2 in a dbt macro to track changes with valid_from / valid_to effective dates, allowing Power BI to report on "who owned what, when" across reorg timelines.
dbt Macro → scd_type_2.sql (reusable across all dimensions):
-- macros/scd_type_2.sql
-- Tracks slowly changing dimension changes with effective dating
-- Usage: {{ scd_type_2(source_model='stg_salesforce_accounts', natural_key='account_id', tracked_columns=['department', 'owner_id', 'territory']) }}
{% macro scd_type_2(source_model, natural_key, tracked_columns) %}
with source as (
select * from {{ ref(source_model) }}
),
changes_detected as (
select
{{ natural_key }},
{% for col in tracked_columns %}
{{ col }},
{% endfor %}
updated_at,
-- hash all tracked columns to detect any change
md5(concat_ws('|',
{% for col in tracked_columns %}
coalesce(cast({{ col }} as varchar), 'NULL')
{{ ',' if not loop.last }}
{% endfor %}
)) as row_hash,
lag(md5(concat_ws('|',
{% for col in tracked_columns %}
coalesce(cast({{ col }} as varchar), 'NULL')
{{ ',' if not loop.last }}
{% endfor %}
))) over (partition by {{ natural_key }}
order by updated_at) as prev_row_hash
from source
),
versioned as (
select
*,
updated_at as valid_from,
coalesce(
lead(updated_at) over (partition by {{ natural_key }}
order by updated_at),
'9999-12-31'::date
) as valid_to,
case when lead(updated_at) over (partition by {{ natural_key }}
order by updated_at) is null
then true else false end as is_current
from changes_detected
where row_hash != coalesce(prev_row_hash, '')
or prev_row_hash is null
)
select * from versioned
{% endmacro %}
The Discovery That Prompted a 60% IT Ticket Reduction
Three months into building the dbt platform, I analyzed the IT data request queue. Of the 400+ monthly tickets, 62% were variations of the same five questions: "How many campaigns are active right now?", "What's the budget utilization for Q3?", "Which campaigns are overrunning?", "Who owns Salesforce account X?", "What's the forecast for Y campaign?" Every analyst was asking the same questions independently because there was no shared, trusted dataset. I built five mart models directly targeting these questions and published them to Power BI. The next month, the same five question types disappeared from the ticket queue entirely. The 60% IT request reduction wasn't a feature — it was a natural byproduct of having a single source of truth that people could access themselves. That experience crystallized why analytics engineering matters: it's not about building dashboards. It's about eliminating the need to ask.