Switchboard Docs
ReferenceSource Models

Klaviyo Models

Copy-paste SQL model definitions for Klaviyo email and SMS reporting.

Copy and Paste these Klaviyo Models into your Data Layer to quickly go from raw data souces —> business ready Klaviyo Reporting. Please note:

  • Models must be added chronological order from top to bottom (so that they can reference each other).
  • When adding your own models you must update the reference to include your source names.

Base

klaviyo_profiles_base

select
  id as profile_id --PK
  , (attributes ->> 'created') :: timestamp as created_at
  , attributes ->> 'first_name' as first_name
  , attributes ->> 'last_name' as last_name
  , attributes ->> 'email' as email
  , attributes ->> 'phone_number' as phone_number
  , attributes ->> 'organization' as organization

  -- ADD: most-used consent + geo, extracted from the JSON you already carry
  , (attributes -> 'subscriptions' -> 'email' -> 'marketing' ->> 'consent') as email_marketing_consent
  , (attributes -> 'subscriptions' -> 'sms' -> 'marketing' ->> 'consent') as sms_marketing_consent
  , (attributes -> 'location' ->> 'country') as country
  , (attributes -> 'location' ->> 'region') as region

  , attributes as attributes_json
  , relationships as relationships_json
  , links as links_json
from {{sources.klaviyo.profiles}}

klaviyo_campaigns_base

select
  id as campaign_id --PK
  , attributes ->> 'name' as campaign_name
  , attributes ->> 'status' as campaign_status
  , (attributes ->> 'archived') :: boolean as is_archived
  , (attributes ->> 'created_at') :: timestamp as created_at
  , (attributes ->> 'scheduled_at') :: timestamp as scheduled_at
  , (attributes ->> 'send_time') :: timestamp as send_time  -- ADD: actual send timestamp

  , attributes as attributes_json
  , relationships as relationships_json
  , links as links_json
from {{sources.klaviyo.campaigns}}

klaviyo_flows_base

select
  id as flow_id --PK
  , attributes ->> 'name' as flow_name
  , attributes ->> 'status' as flow_status
  , (attributes ->> 'archived') :: boolean as is_archived
  , (attributes ->> 'created') :: timestamp as created_at    -- FIX: key is 'created', not 'created_at'
  , (attributes ->> 'updated') :: timestamp as updated_at    -- FIX: flows have 'updated', not 'scheduled_at'
  , attributes ->> 'trigger_type' as trigger_type            -- ADD: flows are triggered, not scheduled

  , attributes as attributes_json
  , relationships as relationships_json
  , links as links_json
from {{sources.klaviyo.flows}}

klaviyo_flow_performance_base

select
  concat(date, '-', flow_message_id) as id --PK
  , (date::timestamptz at time zone '{{ reporting_timezone }}')::date as report_date  -- REPLACE with your account's reporting timezone
  , flow_id
  , flow_message_id
  , send_channel
  , recipients
  , delivered
  , failed
  , bounced
  , opens
  , opens_unique
  , clicks
  , clicks_unique
  , spam_complaints
  , unsubscribes
  , unsubscribe_uniques
  , conversion_uniques
  , conversion_value
  , conversions
  , average_order_value
  , bounce_rate
  , bounced_or_failed
  , bounced_or_failed_rate
  , click_rate
  , click_to_open_rate
  , conversion_rate
  , delivery_rate
  , failed_rate
  , open_rate
  , revenue_per_recipient
  , spam_complaint_rate
  , unsubscribe_rate
from {{sources.klaviyo.reports__flow_performance}}

klaviyo_campaign_performance_base


select
  concat(date, '-', campaign_id) as id --PK
  , (date::timestamptz at time zone '{{ reporting_timezone }}')::date as report_date  -- REPLACE with your account's reporting timezone
  , campaign_id
  , send_channel
  , recipients
  , delivered
  , failed
  , bounced
  , opens
  , opens_unique
  , clicks
  , clicks_unique
  , spam_complaints
  , unsubscribes
  , unsubscribe_uniques
  , conversion_uniques
  , conversion_value
  , conversions
  , average_order_value
  , bounce_rate
  , bounced_or_failed
  , bounced_or_failed_rate
  , click_rate
  , click_to_open_rate
  , conversion_rate
  , delivery_rate
  , failed_rate
  , open_rate
  , revenue_per_recipient
  , spam_complaint_rate
  , unsubscribe_rate
from {{sources.klaviyo.reports__campaign_performance}}

klaviyo_placed_order_events_base

-- Grain: one row per Placed Order event.
with placed_orders as (
  select
    id
    , attributes
    , relationships
    , attributions
  from {{sources.klaviyo.events}}
  where relationships -> 'metric' -> 'data' ->> 'id' = '{{ placed_order_metric_id }}'  -- REPLACE with your account's Placed Order metric ID
)
, attr as (
  select
    id
    , case when jsonb_typeof(attributions) = 'array' and jsonb_array_length(attributions) > 0
           then attributions -> 0 end as first_attr  -- first attribution element only
  from placed_orders
)
select
  po.id as event_id --PK
  , (po.attributes ->> 'datetime')::timestamptz as occurred_at
  , (po.attributes ->> 'timestamp')::bigint as event_timestamp
  , po.relationships -> 'profile' -> 'data' ->> 'id' as profile_id
  , po.relationships -> 'metric' -> 'data' ->> 'id' as metric_id
  , po.attributes -> 'event_properties' ->> '$event_id' as shopify_order_id
  , (po.attributes -> 'event_properties' ->> '$value')::float as order_value
  , po.attributes -> 'event_properties' ->> '$currency_code' as currency_code
  , (po.attributes -> 'event_properties' ->> 'Item Count')::float as item_count
  , po.attributes -> 'event_properties' -> 'Collections' as collections_json
  , po.attributes -> 'event_properties' ->> 'Source Name' as source_name
  , po.attributes -> 'event_properties' ->> 'ShippingRate' as shipping_rate
  , po.attributes -> 'event_properties' -> 'Discount Codes' as discount_codes_json
  , (po.attributes -> 'event_properties' ->> 'Total Discounts')::float as total_discounts
  , po.attributes -> 'event_properties' -> '$extra' as extra_json
  , a.first_attr -> 'relationships' -> 'flow' -> 'data' ->> 'id' as flow_id
  , a.first_attr -> 'relationships' -> 'flow-message' -> 'data' ->> 'id' as flow_message_id
  , a.first_attr -> 'relationships' -> 'campaign' -> 'data' ->> 'id' as campaign_id
  , a.first_attr -> 'relationships' -> 'campaign-message' -> 'data' ->> 'id' as campaign_message_id
  , a.first_attr -> 'relationships' -> 'attributed-event' -> 'data' ->> 'id' as attributed_event_id
  , (a.first_attr is not null) as is_attributed
  , po.attributions as attributions_json
from placed_orders po
left join attr a on a.id = po.id

Fact

klaviyo_daily_send_performance_facts

-- Grain: one row per report_date x send (campaign or flow message).
select
  concat(report_date, '-', campaign_id) as id --PK
  , report_date
  , 'campaign'::text as send_type
  , campaign_id
  , null::text as flow_id
  , null::text as flow_message_id
  , send_channel
  , recipients
  , delivered
  , failed
  , bounced
  , opens_unique as opens
  , clicks_unique as clicks
  , spam_complaints
  , unsubscribe_uniques as unsubscribes
  , conversion_uniques
  , conversion_value as conversion_revenue
  , average_order_value
  , bounce_rate
  , click_rate
  , click_to_open_rate
  , conversion_rate
  , delivery_rate
  , open_rate
  , revenue_per_recipient
  , spam_complaint_rate
  , unsubscribe_rate
from {{models.klaviyo_campaign_performance_base}}
union all
select
  concat(report_date, '-', flow_message_id) as id --PK
  , report_date
  , 'flow'::text as send_type
  , null::text as campaign_id
  , flow_id
  , flow_message_id
  , send_channel
  , recipients
  , delivered
  , failed
  , bounced
  , opens_unique as opens
  , clicks_unique as clicks
  , spam_complaints
  , unsubscribe_uniques as unsubscribes
  , conversion_uniques
  , conversion_value as conversion_revenue
  , average_order_value
  , bounce_rate
  , click_rate
  , click_to_open_rate
  , conversion_rate
  , delivery_rate
  , open_rate
  , revenue_per_recipient
  , spam_complaint_rate
  , unsubscribe_rate
from {{models.klaviyo_flow_performance_base}}

shopify_order_klaviyo_attribuition_facts

This model is Shopify-specific by name and by join — it's built for customers running Klaviyo alongside Shopify, joining Klaviyo's attribution data to shopify_orders_output for order-grain detail (revenue, new vs. returning). Klaviyo itself isn't Shopify-exclusive; a customer on a different commerce platform (Amazon, WooCommerce, BigCommerce, etc.) would need an equivalent model joining to that platform's order facts instead of this one.

with klaviyo_numbered as (
  select
    k.event_id
    , k.occurred_at
    , k.shopify_order_id
    , k.flow_id
    , k.flow_message_id
    , k.campaign_id
    -- earliest event per order; duplicates are identical except event_id
    , row_number() over (partition by k.shopify_order_id order by k.occurred_at asc, k.event_id asc) as order_event_index
  from {{models.klaviyo_placed_order_events_base}} k
)
, klaviyo_attributed as (
  select
    shopify_order_id
    , occurred_at
    , flow_id
    , flow_message_id
    , campaign_id
  from klaviyo_numbered
  where order_event_index = 1
  and (flow_id is not null or campaign_id is not null)
)
select
  k.shopify_order_id as order_id --PK
  , k.occurred_at
  , o.first_time_or_returning
  , case 
			  when k.flow_id is not null 
				  then 'flow' 
				  else 'campaign'
		end as attribution_channel
  , k.flow_id
  , k.flow_message_id
  , k.campaign_id
  , o.order_total_line_item_net_revenue as net_revenue
from klaviyo_attributed k
left join {{models.shopify_orders_output}} o on o.order_id = k.shopify_order_id

Output

klaviyo_send_performance_output


-- order_rollup: Klaviyo-attributed orders rolled up to send key (flow_message_id
-- or campaign_id) x order conversion date. Joined below on BOTH key and date
-- (full outer join) so late-converting orders (no matching send report_date)
-- still land in the output on their own conversion date.
with order_rollup as (
  select
    flow_id
    , flow_message_id
    , campaign_id
    , (occurred_at at time zone '{{ reporting_timezone }}')::date as order_date
    , count(*) as n_attributed_orders
    , count(*) filter (where first_time_or_returning = 'First-time') as n_first_time_orders
    , count(*) filter (where first_time_or_returning = 'Returning') as n_returning_orders
    , count(*) filter (where first_time_or_returning = 'Zero dollar orders') as n_zero_dollar_orders
    , count(*) filter (where first_time_or_returning is null) as n_unmatched_orders
    , sum(net_revenue) as attributed_revenue
  from {{models.shopify_order_klaviyo_attribuition_facts}}
  group by
    flow_id
    , flow_message_id
    , campaign_id
    , (occurred_at at time zone '{{ reporting_timezone }}')::date
)
select
  coalesce(s.id, concat(r.order_date, '-', coalesce(r.flow_message_id, r.campaign_id))) as id --PK
  , coalesce(s.report_date, r.order_date) as report_date
  , coalesce(s.send_type, case when r.flow_id is not null then 'flow' else 'campaign' end) as send_type
  , coalesce(s.campaign_id, r.campaign_id) as campaign_id
  , coalesce(s.flow_id, r.flow_id) as flow_id
  , coalesce(s.flow_message_id, r.flow_message_id) as flow_message_id
  , s.send_channel
  , s.recipients
  , s.delivered
  , s.failed
  , s.bounced
  , s.opens
  , s.clicks
  , s.spam_complaints
  , s.unsubscribes
  , s.conversion_uniques
  , s.conversion_revenue
  , s.average_order_value
  , s.bounce_rate
  , s.click_rate
  , s.click_to_open_rate
  , s.conversion_rate
  , s.delivery_rate
  , s.open_rate
  , s.revenue_per_recipient
  , s.spam_complaint_rate
  , s.unsubscribe_rate
  , r.n_attributed_orders
  , r.n_first_time_orders
  , r.n_returning_orders
  , r.n_zero_dollar_orders
  , r.n_unmatched_orders
  , r.attributed_revenue
from {{models.klaviyo_daily_send_performance_facts}} s
full outer join order_rollup r
  on coalesce(r.flow_message_id, r.campaign_id) = coalesce(s.flow_message_id, s.campaign_id)
  and r.order_date = s.report_date

✅ Show model in documents

Aggregations

  • Attributed Revenue

    • Property: Attributed Revenue
    • Operator: Sum
  • Attributed Revenue Per Recipient

    • Custom Formula

      COUNT(DISTINCT
        CASE 
           WHEN {{prop.stay_ai_subscriptions_output.churned_at}} IS NOT NULL 
             THEN {{prop.stay_ai_subscriptions_output.subscription_id }} 
         END)
  • Avg Bounce Rate

    • Property: Bounce Rate
    • Operator: Average
  • Avg Click Rate

    • Property: Click Rate
    • Operator: Average
  • Avg Click-to-Open Rate

    • Property: Click to Open Rate
    • Operator: Average
  • Avg Conversion Rate

    • Property: Conversion Rate
    • Operator: Average
  • Avg Open Rate

    • Property: Open Rate
    • Operator: Average
  • Avg Revenue Per Recipient

    • Property: Revenue Per Recipient
    • Operator: Average
  • Avg Unsubscribe Rate

    • Property: Unsubscribe Rate
    • Operator: Average
  • Conversion Revenue

    • Property: Conversion Revenue
    • Operator: Sum
  • Conversion Uniques

    • Property: Conversion Uniques
    • Operator: Sum
  • First-time Orders

    • Property: N First Time Orders
    • Operator: Sum
  • Returning Orders

    • Property: N Returning Orders
    • Operator: Sum
  • Total Attributed Orders

    • Property: N Attributed Orders
    • Operator: Sum
  • Total Bounced

    • Property: Bounced
    • Operator: Sum
  • Total Clicks

    • Property: Clicks
    • Operator: Sum
  • Total Delivered

    • Property: Delivered
    • Operator: Sum
  • Total Opens

    • Property: Opens
    • Operator: Sum
  • Total Recipients

    • Property: Recipients
    • Operator: Sum
  • Total Spam Complaints

    • Property: Spam Complaints
    • Operator: Sum
  • Total Unsubscribes

    • Property: Unsubscribes
    • Operator: Sum
  • Unmatched Orders

    • Property: N Unmatched Orders
    • Operator: Sum
  • Zero-dollar/Cancelled Orders

    • Property: N Zero Dollar Orders
    • Operator: Sum

klaviyo_flows_output

select
  flow_id --PK
  , flow_name
  , flow_status
  , is_archived
  , created_at
  , updated_at
from {{models.klaviyo_flows_base}}

✅ Show model in documents

Aggregations

  • Total Flows
    • Property: Flow Id
    • Operator: Count

klaviyo_campaigns_output

SELECT
  campaign_id --pk
  , campaign_name
  , campaign_status
  , is_archived
  , created_at
  , scheduled_at
FROM {{models.klaviyo_campaigns_base}}

✅ Show model in documents

Aggregations

  • Total Campaigns
    • Property: Campaign Id
    • Operator: Count

On this page