Event Measurements
Cross-source quantitative analysis through the measurements facet — extract multiple business measurements from events using the standard EAV pattern.
Event measurements bring quantitative business concepts — revenue, tax, expenses, hours, counts, scores — into the nexus compressed schema, making them queryable across sources without source-specific knowledge.
Measurements vs Metrics
These terms are often conflated (Cube and other semantic layers use them interchangeably), but they are distinct concepts with roots in formal measurement theory.
- Measurement: A specific quantitative observation tied to a specific event. "This invoice's revenue was $100." Measurements live in the data. They are nexus's responsibility.
- Metric: An aggregation or arithmetic operation over measurements. "Total revenue this quarter was $50,000." or "Average invoice revenue was $200." Metrics are computed at query time. They are the semantic layer's responsibility (or the LLM's, or the analyst's).
Nexus outputs measurements. Whatever sits on top — Cube, an LLM, a dashboard — computes metrics from those measurements.
Why "Measurement"?
The terminology is deliberate and grounded in measurement science (metrology).
A measurement is an observation, not ground truth. In metrology — the formal science of measurement standardized by NIST and the BIPM — every measurement has inherent uncertainty. An invoice amount of $100 is really "$100 as observed by FreshBooks at this point in time, subject to future adjustments." Credit notes, refunds, currency rounding, and timing differences between systems all introduce uncertainty. The measurement captures what was observed at extraction time.
A metric is a derived quantity. Metrology distinguishes between direct
measurements (observations from a source) and derived quantities (computed from
measurements via an equation). SUM(revenue measurements) is a derived
quantity. The uncertainty from individual measurements propagates into the
aggregate — if one invoice is later adjusted, the total changes too.
Measurements carry dimensional metadata. Dimensional analysis — the branch
of mathematics concerned with units — tells us that you can only add quantities
with the same dimensions. Revenue in USD + Revenue in CAD requires conversion.
Revenue + Hours is meaningless. The value_unit column carries this dimensional
metadata, making each measurement self-describing and preventing invalid
aggregations.
Measurements exist on specific scales. Stevens' Scales of Measurement (1946) classify data by what operations are valid:
| Scale | Property | Valid Operations | Example Measurements |
|---|---|---|---|
| Ratio | True zero, meaningful distances | SUM, AVG, ratios | revenue, hours, weight |
| Interval | No true zero, meaningful distances | AVG, differences | temperature_celsius |
| Ordinal | Ordered, no meaningful distances | Rank, median | satisfaction_score |
| Nominal | Categories only | Count, mode | (not measurements — these are traits) |
Most business measurements (revenue, hours, amounts) are ratio scale — summing them is valid. But the distinction matters when measurements include scores or ratings where summation is technically invalid.
This formal grounding ensures the terminology is precise: measurements are observations with uncertainty, units, and scale properties. Metrics are derived from measurements. Nexus handles the former; the query layer handles the latter.
The Problem
A single business record often carries multiple quantitative dimensions. A
Stripe invoice has total, subtotal, tax, amount_paid, amount_remaining
— five distinct financial measurements from one record. A FreshBooks expense has
amount and tax_amount_1. An Upwork transaction has amount but the meaning
depends on transaction_type (earnings vs service fees).
Without measurements, answering "What was my revenue for the past 6 years?" requires breaking out of the nexus compressed schema and dropping back to source-specific tables -- joining individual event models, knowing which source/event_name combinations constitute "revenue," what transformations are needed (Stripe amounts are in cents), and how to avoid double-counting. It's brittle, non-portable across clients, and impossible for an LLM to infer without deep domain knowledge.
Measurements close this gap.
The Solution: Measurements as a Facet
Measurements follow the same faceted extraction pattern as identifiers, traits, states, and attribution. The intermediate EAV table stores:
- The entity is the event (
event_id) - The attribute is the measurement name (
measurement_name) - The value is the quantitative payload (
value+value_unit)
A single event can produce multiple measurement rows, each from a different source column with a different semantic meaning:
Source Record: Stripe Invoice #in_abc123 (paid)
→ measurement: measurement_name = 'revenue', value = 100.00, value_unit = 'usd'
→ measurement: measurement_name = 'subtotal', value = 92.00, value_unit = 'usd'
→ measurement: measurement_name = 'tax', value = 8.00, value_unit = 'usd'
Source Record: FreshBooks Expense #456
→ measurement: measurement_name = 'expense', value = -50.00, value_unit = 'cad'
→ measurement: measurement_name = 'tax_paid', value = -6.50, value_unit = 'cad'
Source Record: Upwork Transaction #789 (Fixed-price)
→ measurement: measurement_name = 'revenue', value = 500.00, value_unit = 'usd'
Events themselves carry no numeric value column -- quantitative data lives
entirely in measurements. Measurements capture all quantitative dimensions
of an event, each with its own name, value, and unit.
Schema
Core Table: nexus_event_measurements
The analyst-facing table. One row per event, one column per measurement type.
Measurement columns are discovered dynamically at compile time — new measurement
types from new sources appear automatically on the next dbt build.
| Column | Type | Required | Description |
|---|---|---|---|
event_id |
STRING | ✅ | FK to the event |
occurred_at |
TIMESTAMP | ✅ | When (denormalized from event) |
source |
STRING | ✅ | Source system |
{measurement_name} |
NUMERIC | ❌ | One column per measurement (e.g., revenue, tax, expense). NULL if not applicable. |
The event_id linkage connects measurements to the full nexus entity graph
through Events → Participants → Entities, enabling queries like "Revenue by
customer" without any source-specific joins.
Units are available in nexus_event_measurements_metadata (one row per
measurement_name/source combination with unit, count, and value statistics).
Intermediate Table: nexus_event_measurements_unioned
The EAV canonical form. One row per measurement per event. This is what source models feed into and what the core table pivots from.
| Column | Type | Required | Description |
|---|---|---|---|
event_measurement_id |
STRING | ✅ | Unique ID (evt_msr_ prefix) |
event_id |
STRING | ✅ | FK to the event this measurement came from |
measurement_name |
STRING | ✅ | What was measured (the EAV discriminator) |
value |
NUMERIC | ✅ | The observed quantitative value |
value_unit |
STRING | ✅ | Unit of measurement (currency, hours, count) |
occurred_at |
TIMESTAMP | ✅ | When (denormalized from event) |
source |
STRING | ✅ | Source system |
You should not need to query this table directly — use the core table instead.
Extraction Pattern
Measurements follow the standard four-layer architecture described in Recommended Source Model Structure.
Directory Structure
models/sources/{source_name}/
├── intermediate/
│ ├── {source}_invoice_events.sql ← existing
│ ├── {source}_invoice_measurements.sql ← NEW
│ ├── {source}_expense_events.sql ← existing
│ ├── {source}_expense_measurements.sql ← NEW
│ ├── {source}_person_identifiers.sql ← existing
│ └── {source}_group_identifiers.sql ← existing
├── {source}_events.sql ← existing (unions events)
├── {source}_entity_identifiers.sql ← existing (unions identifiers)
├── {source}_event_measurements.sql ← NEW (unions measurements)
Intermediate Measurement Models
Each intermediate measurement model:
- Reads from intermediate event models for
event_idlinkage and timing - Joins to normalized tables for additional financial columns not on the event
- Unpivots multiple quantitative columns into separate measurement rows
This mirrors how identifier models read from events and extract multiple identifier rows per event.
Example: stripe_invoice_measurements.sql
{{ config(
materialized='table',
tags=['stripe', 'intermediate', 'measurements']
) }}
-- Unpivot financial dimensions from Stripe invoices into measurement rows
-- One invoice paid event → multiple measurements (revenue, subtotal, tax)
WITH invoice_paid_events AS (
SELECT event_id, invoice_id, occurred_at, source
FROM {{ ref('stripe_invoice_historical_events') }}
WHERE event_name = 'invoice paid'
),
invoices AS (
SELECT * FROM {{ ref('stripe_invoices') }}
),
-- Join events to normalized invoices for full financial detail
event_financials AS (
SELECT
e.event_id,
e.occurred_at,
e.source,
CAST(i.total AS NUMERIC) / 100 as total,
CAST(i.subtotal AS NUMERIC) / 100 as subtotal,
CAST(i.total - i.subtotal AS NUMERIC) / 100 as tax,
i.currency
FROM invoice_paid_events e
JOIN invoices i ON e.invoice_id = i.id
),
revenue AS (
SELECT
{{ nexus.create_nexus_id('event_measurement', ['event_id', "'revenue'"]) }}
as event_measurement_id,
event_id,
'revenue' as measurement_name,
total as value,
currency as value_unit,
occurred_at,
source
FROM event_financials
),
subtotal_measurement AS (
SELECT
{{ nexus.create_nexus_id('event_measurement', ['event_id', "'subtotal'"]) }}
as event_measurement_id,
event_id,
'subtotal' as measurement_name,
subtotal as value,
currency as value_unit,
occurred_at,
source
FROM event_financials
),
tax_measurement AS (
SELECT
{{ nexus.create_nexus_id('event_measurement', ['event_id', "'tax'"]) }}
as event_measurement_id,
event_id,
'tax' as measurement_name,
tax as value,
currency as value_unit,
occurred_at,
source
FROM event_financials
WHERE tax > 0
)
SELECT * FROM revenue
UNION ALL
SELECT * FROM subtotal_measurement
UNION ALL
SELECT * FROM tax_measurement
Example: freshbooks_expense_measurements.sql
{{ config(
materialized='table',
tags=['freshbooks', 'intermediate', 'measurements']
) }}
-- Unpivot financial dimensions from FreshBooks expenses into measurement rows
-- One expense event → expense amount + tax paid
WITH expense_events AS (
SELECT * FROM {{ ref('freshbooks_expense_events') }}
),
expenses AS (
SELECT * FROM {{ ref('freshbooks_expenses') }}
),
event_financials AS (
SELECT
e.event_id,
e.occurred_at,
e.source,
exp.amount,
exp.tax_amount_1,
exp.amount_code
FROM expense_events e
JOIN expenses exp ON CAST(exp.id AS STRING) = e.invoice_id
),
expense_amount AS (
SELECT
{{ nexus.create_nexus_id('event_measurement', ['event_id', "'expense'"]) }}
as event_measurement_id,
event_id,
'expense' as measurement_name,
-1 * amount as value,
amount_code as value_unit,
occurred_at,
source
FROM event_financials
),
tax_paid_measurement AS (
SELECT
{{ nexus.create_nexus_id('event_measurement', ['event_id', "'tax_paid'"]) }}
as event_measurement_id,
event_id,
'tax_paid' as measurement_name,
-1 * tax_amount_1 as value,
amount_code as value_unit,
occurred_at,
source
FROM event_financials
WHERE tax_amount_1 IS NOT NULL AND tax_amount_1 > 0
)
SELECT * FROM expense_amount
UNION ALL
SELECT * FROM tax_paid_measurement
Source-Level Union: stripe_event_measurements.sql
{{ config(
materialized='table',
tags=['nexus', 'measurements', 'stripe']
) }}
-- Union all Stripe measurement types
{{ dbt_utils.union_relations(
relations=[
ref('stripe_invoice_measurements')
]
) }}
ORDER BY occurred_at DESC
Querying Measurements
The core nexus_event_measurements table is pivoted — each measurement type is
a column. No WHERE measurement_name = ... needed.
Simple Aggregation
-- "What was my revenue for the past 6 years?"
SELECT
EXTRACT(YEAR FROM occurred_at) AS year,
SUM(revenue) AS total_revenue
FROM nexus_event_measurements
GROUP BY 1
ORDER BY 1
Multi-Measurement Comparison
-- "Show me revenue vs expenses by quarter"
SELECT
DATE_TRUNC(occurred_at, QUARTER) AS quarter,
SUM(revenue) AS total_revenue,
SUM(expense) AS total_expenses
FROM nexus_event_measurements
GROUP BY 1
ORDER BY 1
Derived Metrics
-- "What's my profit?" (expenses are already negative)
SELECT
EXTRACT(YEAR FROM occurred_at) AS year,
SUM(revenue) + SUM(expense) AS profit
FROM nexus_event_measurements
GROUP BY 1
ORDER BY 1
Entity-Attributed Measurements
-- "Revenue by customer"
SELECT
nexus_entities.name AS customer,
SUM(nexus_event_measurements.revenue) AS total_revenue
FROM nexus_event_measurements
JOIN nexus_entity_participants
ON nexus_event_measurements.event_id = nexus_entity_participants.event_id
JOIN nexus_entities
ON nexus_entity_participants.entity_id = nexus_entities.entity_id
WHERE nexus_entities.entity_type = 'group'
GROUP BY 1
ORDER BY 2 DESC
Invoice Detail
-- "Give me a table of my invoices with revenue and tax"
SELECT
event_id,
occurred_at,
source,
revenue,
tax
FROM nexus_event_measurements
WHERE revenue IS NOT NULL
ORDER BY occurred_at DESC
Discoverability
-- "What measurements are available and in what units?"
SELECT * FROM nexus_event_measurements_metadata
Measurements Are Not Just Financial
While revenue, tax, and expenses are the motivating examples, measurements are general-purpose. Any quantitative dimension of an event can be a measurement:
| Domain | Example Measurements |
|---|---|
| Finance | revenue, tax, subtotal, expense, platform_fee |
| Time | hours_worked, resolution_time, time_to_close |
| Usage | api_calls, data_transferred_gb, active_users |
| Sales | deal_amount, probability, discount_amount |
| Healthcare | copay, insurance_reimbursement, patient_payment |
| E-commerce | order_total, shipping_cost, refund_amount |
The schema is the same regardless of domain. The measurement_name
discriminator and value_unit carry the semantics.
Configuration
Configure measurement sources in dbt_project.yml:
vars:
nexus:
sources:
stripe:
enabled: true
events: true
measurements: true
entities: ['person', 'group']
freshbooks:
enabled: true
events: true
measurements: true
entities: ['person', 'group']
upwork:
enabled: true
events: true
measurements: true
entities: ['person', 'group']
The nexus package automatically unions all enabled source measurements into
nexus_event_measurements.
Metadata
The nexus_event_measurements_metadata table catalogs available measurements:
SELECT * FROM nexus_event_measurements_metadata
This metadata table is automatically maintained and serves as the LLM-queryable catalog of what measurements exist, what units they use, and which sources contribute them.
Measurements and the Semantic Layer
Measurements are almost a semantic layer — but intentionally not one.
A traditional semantic layer (Cube, LookML, dbt Metrics) defines what can be measured, how to aggregate it, what dimensions to group by, and provides a query API. Nexus measurements handle the hardest part of that problem — cross-source extraction and labeling — and leave the rest to whatever query layer sits on top.
| Responsibility | Nexus Measurements | Semantic Layer |
|---|---|---|
| What IS revenue across 5 sources? | ✅ | ❌ |
| Unit tagging and embedded semantics | ✅ | ❌ |
| Entity linkage through event graph | ✅ | ❌ |
| Discoverability via metadata tables | ✅ | ❌ |
| Canonical metric definitions | ✅ (metadata) | ✅ |
| Aggregation function (SUM vs AVG) | ❌ | ✅ |
| Dimension definitions (group-by axes) | ❌ | ✅ |
| Time intelligence (YoY, MTD, rolling) | ❌ | ✅ |
| Derived metrics (profit = rev + exp) | ❌ | ✅ |
| Query API / SQL generation | ❌ | ✅ |
This boundary is intentional. Nexus is a transformation package — it should output clean, standardized, context-rich data and handle as much of that work as possible. Nexus does provide canonical metric definitions as a metadata table — the recipes for computing business numbers — but the actual aggregation, time intelligence, and SQL generation remain concerns of whatever query layer sits on top: an LLM, Cube, a BI tool, or a human writing SQL.
The cleaner the data substrate, the less any semantic layer has to do, and the more interchangeable it becomes. This is the commoditized complement strategy: nexus makes the data layer so clean that the query layer becomes a lightweight, swappable commodity.
Relationship to Other Facets
Measurements complete the nexus facet set:
erDiagram
EVENTS ||--o{ IDENTIFIERS : "who was involved"
EVENTS ||--o{ TRAITS : "what do we know about them"
EVENTS ||--o{ MEASUREMENTS : "what was the quantitative impact"
EVENTS ||--o{ STATES : "what state changed"
EVENTS ||--o{ ATTRIBUTION : "what caused this"
EVENTS ||--o{ RELATIONSHIPS : "what connections exist"
All facets link to events via event_id. Events link to entities via
participants. This means every facet is automatically attributable to entities
without source-specific joins.
Related Documentation
- Event Dimensions — Categorical filters (the companion facet to measurements)
- Metrics — Canonical business calculations built from measurements and dimensions
- Architecture Overview — Core tables ERD and key joins
- Pipeline Structure — Sources, Intermediate, and Core stages
- Recommended Source Model Structure — Four-layer extraction architecture
- Format Nexus Events — Event schema reference