Jaco van der Laan

From Entity to Measure: Automated DAX Generation

09/03/2026  ·  Data Analysis  ·  PowerBI  ·  SQL Code Generation

power-bi
‹ ›

Rule-based measure creation that scales


From Entity to Measure: Automated DAX Generation

Rule-based measure creation that scales


Every Power BI semantic model needs measures. Total Sales. Average Order Value. Year-over-Year Growth. Count of Customers.

Most teams write these by hand. Every. Single. Time.

But measures follow patterns. If you can describe the pattern, you can generate the measure. This article shows how model-driven data engineering transforms entity definitions into complete DAX measures — automatically.


The Measure Explosion Problem

Consider a simple entity: Sales with one numeric attribute: Amount.

From this single attribute, business users need:

That’s 10 measures from one attribute. A typical model has 20–50 numeric attributes. Do the math.

Writing these by hand is:

  • Slow — Hours of repetitive work
  • Error-prone — Typos, inconsistent naming, wrong date tables
  • Unmaintainable — Rename an attribute, update 50 measures

The Pattern Recognition

Look at those measures again. They follow templates:

Aggregation Patterns

SUM([{attribute}])  
AVERAGE([{attribute}])  
MIN([{attribute}])  
MAX([{attribute}])  
COUNTROWS({table})  
DISTINCTCOUNT([{attribute}])

Time Intelligence Patterns

CALCULATE([{base_measure}], DATESYTD('{date_table}'[{date_column}]))  
CALCULATE([{base_measure}], DATESQTD('{date_table}'[{date_column}]))  
CALCULATE([{base_measure}], DATESMTD('{date_table}'[{date_column}]))  
CALCULATE([{base_measure}], SAMEPERIODLASTYEAR('{date_table}'[{date_column}]))

Comparison Patterns

[{base_measure}] - [{comparison_measure}]  
DIVIDE([{base_measure}] - [{comparison_measure}], [{comparison_measure}])

If measures follow patterns, we can generate them from rules.


The MDDE Approach

Step 1: Define the Entity

# entities/sales.yaml  
entities:  
  - name: Sales  
    stereotype: dim_fact  
    description: "Customer sales transactions"

date_role: OrderDate  # Which column links to date dimension  
    attributes:  
      - name: OrderDate  
        type: date  
        role: date_key  
        references: Date.DateKey  
      - name: Amount  
        type: decimal(18,2)  
        description: "Net transaction amount"  
        semantic_type: currency  
        aggregations: [sum, average]  # Which aggregations to generate  
      - name: Quantity  
        type: int  
        description: "Number of units sold"  
        aggregations: [sum]  
      - name: CustomerID  
        type: int  
        references: Customer.CustomerID  
        aggregations: [distinctcount]  # Count unique customers

Step 2: Define Measure Templates

# templates/time_intelligence.yaml  
templates:  
  - name: YTD  
    suffix: " YTD"  
    expression: |  
      CALCULATE(  
          [{base_measure}],  
          DATESYTD('{date_table}'[{date_column}])  
      )  
    description_prefix: "Year-to-date:"

- name: QTD  
    suffix: " QTD"  
    expression: |  
      CALCULATE(  
          [{base_measure}],  
          DATESQTD('{date_table}'[{date_column}])  
      )  
    description_prefix: "Quarter-to-date:"  
  - name: PY  
    suffix: " PY"  
    expression: |  
      CALCULATE(  
          [{base_measure}],  
          SAMEPERIODLASTYEAR('{date_table}'[{date_column}])  
      )  
    description_prefix: "Prior year:"  
  - name: YoY  
    suffix: " YoY"  
    expression: |  
      VAR CurrentValue = [{base_measure}]  
      VAR PriorValue = [{base_measure} PY]  
      RETURN  
          DIVIDE(  
              CurrentValue - PriorValue,  
              PriorValue  
          )  
    description_prefix: "Year-over-year change:"  
    format: percent  
    requires: [PY]  # Must generate PY first

Step 3: Generate Measures

mdde dax generate --entity Sales --output measures.tmdl

Generated output:

 // ============================================  
// Auto-generated measures for entity: Sales  
// Generated: 2026-03-07T14:30:00Z  
// Source: entities/sales.yaml  
// ============================================

// Base aggregations  
measure 'Total Revenue' = SUM(Sales[Amount])  
    formatString: \$#,0.00  
    description: "Sum of net transaction amount"  
measure 'Average Transaction' = AVERAGE(Sales[Amount])  
    formatString: \$#,0.00  
    description: "Average of net transaction amount"  
measure 'Total Quantity' = SUM(Sales[Quantity])  
    formatString: #,0  
    description: "Sum of number of units sold"  
measure 'Customer Count' = DISTINCTCOUNT(Sales[CustomerID])  
    formatString: #,0  
    description: "Count of unique customers"  
// Time intelligence: Total Revenue  
measure 'Total Revenue YTD' =  
    CALCULATE(  
        [Total Revenue],  
        DATESYTD('Date'[Date])  
    )  
    formatString: \$#,0.00  
    description: "Year-to-date: Sum of net transaction amount"  
measure 'Total Revenue QTD' =  
    CALCULATE(  
        [Total Revenue],  
        DATESQTD('Date'[Date])  
    )  
    formatString: \$#,0.00  
    description: "Quarter-to-date: Sum of net transaction amount"  
measure 'Total Revenue PY' =  
    CALCULATE(  
        [Total Revenue],  
        SAMEPERIODLASTYEAR('Date'[Date])  
    )  
    formatString: \$#,0.00  
    description: "Prior year: Sum of net transaction amount"  
measure 'Total Revenue YoY' =  
    VAR CurrentValue = [Total Revenue]  
    VAR PriorValue = [Total Revenue PY]  
    RETURN  
        DIVIDE(  
            CurrentValue - PriorValue,  
            PriorValue  
        )  
    formatString: 0.0%  
    description: "Year-over-year change: Sum of net transaction amount"  
// Time intelligence: Total Quantity  
measure 'Total Quantity YTD' =  
    CALCULATE(  
        [Total Quantity],  
        DATESYTD('Date'[Date])  
    )  
    formatString: #,0  
    description: "Year-to-date: Sum of number of units sold"  
// ... (additional measures)

From 4 attribute definitions, we generated 16+ measures — all correctly named, formatted, and documented.


Rule Categories

Category 1: Base Aggregations

# rules/aggregations.yaml  
aggregation_rules:  
  - semantic_type: currency  
    default: sum  
    also_generate: [average]  
    format: "$#,0.00"  
  - semantic_type: quantity  
    default: sum  
    format: "#,0"  
  - semantic_type: rate  
    default: average  
    format: "0.00%"  
  - semantic_type: identifier  
    default: distinctcount  
    format: "#,0"

Category 2: Time Intelligence

# rules/time_intelligence.yaml  
time_intelligence:  
  - name: ytd_qtd_mtd  
    patterns: [YTD, QTD, MTD]  
    applies_to: [currency, quantity]  
  - name: prior_period  
    patterns: [PY, PQ, PM]  
    applies_to: [currency, quantity]  
  - name: period_change  
    patterns: [YoY, QoQ, MoM]  
    applies_to: [currency, quantity]  
    requires: prior_period  
    format: percent  
  - name: rolling  
    patterns:  
      - name: R12M  
        months: 12  
      - name: R3M  
        months: 3  
    applies_to: [currency]

Category 3: Ratios and Percentages

PatternGenerated MeasureExamplecontribution% of ParentProduct % of Category Salesshare% of TotalMarket SharemarginA / BGross Margin %conversionA / BConversion Rate

# rules/ratios.yaml  
ratio_rules:  
  - name: margin  
    numerator: profit  
    denominator: revenue  
    format: percent  
    template: |  
      DIVIDE([{numerator}], [{denominator}])- name: contribution

    template: |  
      DIVIDE(  
          [{measure}],  
          CALCULATE([{measure}], ALLSELECTED('{dimension}'))  
      )  
    format: percent

Category 4: Statistical


Smart Defaults

The generator infers sensible defaults from metadata:

Date Table Detection

# If entity has date_role, use it  
entities:  
  - name: Sales  
    date_role: OrderDate  # Generator uses 'Date' table linked here

Format Inference

# Semantic type determines format  
attributes:  
  - name: Amount  
    semantic_type: currency  # → "$#,0.00"

  - name: Rate  
    semantic_type: percentage  # → "0.0%"

  - name: Count  
    semantic_type: integer  # → "#,0"

Description Generation

# Descriptions compose from parts  
measures:  
  - name: Total Revenue  
    # Description: "Sum of net transaction amount"  
    # From: aggregation(sum) + attribute.description

  - name: Total Revenue YTD  
    # Description: "Year-to-date: Sum of net transaction amount"  
    # From: template.description_prefix + base_measure.description

Custom Measures

Not everything fits a template. Custom measures coexist with generated ones:

entities:  
  - name: Sales  
    attributes:  
      - name: Amount  
        aggregations: [sum]  # Generate standard measure

    measures:  
      # Custom measure (passed through verbatim)  
      - name: "Weighted Average Price"  
        expression: |  
          DIVIDE(  
              SUMX(Sales, Sales[Amount] * Sales[Quantity]),  
              SUM(Sales[Quantity])  
          )  
        description: "Revenue-weighted average unit price"

      # Reference generated measure in custom measure  
      - name: "Revenue vs Target"  
        expression: |  
          [Total Revenue] - [Revenue Target]  
        description: "Variance from target revenue"

CLI Commands

# Generate measures for one entity  
mdde dax generate --entity Sales

# Generate for all entities  
mdde dax generate --all

# Preview without writing files  
mdde dax generate --entity Sales --dry-run

# Generate specific categories only  
mdde dax generate --entity Sales --patterns time_intelligence,ratios

# Export as TMDL  
mdde dax generate --entity Sales --format tmdl --output ./tmdl/

# Export as DAX script (for Tabular Editor)  
mdde dax generate --entity Sales --format dax --output measures.dax

Validation

Generated measures are validated before output:

MEASURE GENERATION REPORT  
=========================

Entity: Sales  
Generated: 24 measures  
Validation:  
  ✓ All base measures reference valid columns  
  ✓ Time intelligence measures reference valid date table  
  ✓ Ratio denominators are non-null  
  ✓ No circular references detected  
  ✓ All formats are valid DAX format strings  
Warnings:  
  ⚠ Measure "Customer Count YTD" may perform poorly  
    (DISTINCTCOUNT + time intelligence)  
    Consider: Use approximate count or pre-aggregate  
Generated successfully.

Integration with TMDL Export

Measure generation integrates with the full TMDL export pipeline:

# Generate complete semantic model  
mdde export powerbi \  
    --format tmdl \  
    --output ./tmdl/ \  
    --generate-measures \  
    --time-intelligence ytd,py,yoy \  
    --include-ratios

The exported TMDL includes:

  • Tables from entity definitions
  • Columns with proper data types
  • Relationships from references
  • Generated measures from rules
  • Custom measures from metadata
  • AI-ready properties (descriptions, synonyms)

Real-World Example

Input: E-commerce Entity Model

entities:  
  - name: Orders  
    stereotype: dim_fact  
    date_role: OrderDate  
    attributes:  
      - name: OrderDate  
        type: date  
        role: date_key  
      - name: Revenue  
        type: decimal(18,2)  
        semantic_type: currency  
        aggregations: [sum, average]  
      - name: Quantity  
        type: int  
        semantic_type: quantity  
        aggregations: [sum]  
      - name: Discount  
        type: decimal(18,2)  
        semantic_type: currency  
        aggregations: [sum]  
      - name: CustomerID  
        type: int  
        aggregations: [distinctcount]  
      - name: OrderID  
        type: int  
        role: primary_key  
        aggregations: [count]  
    time_intelligence: [ytd, py, yoy]  
    ratios:  
      - name: Average Order Value  
        expression: "[Total Revenue] / [Order Count]"  
      - name: Discount Rate  
        expression: "[Total Discount] / [Total Revenue]"

Output: 28 Generated Measures

28 measures from 6 attribute definitions. All correctly named, formatted, documented.


Key Takeaways

  1. Measures follow patterns — If you can describe the pattern, you can generate the measure.
  2. Semantic types drive defaults — Currency gets $#,0.00, quantities get #,0, percentages get 0.0%.
  3. Time intelligence multiplies — One base measure generates YTD, QTD, MTD, PY, YoY variants automatically.
  4. Custom measures coexist — Templates for common patterns, custom DAX for unique logic.
  5. Validation catches errors — Invalid references, circular dependencies, performance issues flagged before deployment.
  6. Integration is seamless — Measure generation is part of the TMDL export pipeline.

Stop writing measures by hand. Define patterns, generate DAX, and focus on the business logic that actually requires human thought.


This is part of the Power BI + MDDE integration series.

Previous: “ MCP: The Universal Language for Data Tools ” Next: “ Copilot Optimization Checklist ”