Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Get Fabric Certified for FREE during AI Skills Fest. This week only. Secure your voucher now.

techies

DAX User-Defined Functions: Reusable Logic, Typed Parameters, and Smarter Evaluation

If you have spent time building activity-tracking models in Power BI, you know the pain: the same filter-calculate-max pattern appears in measure after measure, slightly tweaked each time. Many DAX models suffered from duplicate logic across measures, difficult debugging, inconsistent calculations , long, unreadable measures, and hard-to-maintain semantic models. One bug, and you are hunting it across a dozen places. DAX User-Defined Functions (UDFs) change that. Introduced with Microsoft Fabric semantic modeling enhancements, you can now encapsulate the same logic once in a reusable function and invoke it anywhere, instead of repeating it across multiple measures, calculated tables, or queries.

 

Understanding the Base Scenario

We have a table named user_dailyactivitydate. The objective is to determine the current activity streak for each user.

The logic works like this:

  1. Latest activity date.
  2. Find the run group associated with that date
  3. Return the maximum consecutive days for that run group

 

Step 1 — DAX Without UDFs

 

EVALUATE
ADDCOLUMNS (
    SUMMARIZE (
        user_dailyactivitydate,
        user_dailyactivitydate[userid]
    ),

    "current_streak",

    VAR CurrentUser =
        user_dailyactivitydate[userid]

    VAR LastActivityDate =
        CALCULATE (
            MAX ( user_dailyactivitydate[ActivityDate] ),
            FILTER (
                ALL ( user_dailyactivitydate ),
                user_dailyactivitydate[userid] = CurrentUser
            )
        )

    VAR LastRunGroup =
        CALCULATE (
            MAX ( user_dailyactivitydate[run_group] ),
            FILTER (
                ALL ( user_dailyactivitydate ),
                user_dailyactivitydate[userid] = CurrentUser
                    && user_dailyactivitydate[ActivityDate] = LastActivityDate
            )
        )

    RETURN
        CALCULATE (
            MAX ( user_dailyactivitydate[consecutive_days] ),
            FILTER (
                ALL ( user_dailyactivitydate ),
                user_dailyactivitydate[userid] = CurrentUser
                    && user_dailyactivitydate[run_group] = LastRunGroup
            )
        )
)

 

This DAX code works perfectly.

But imagine using this same logic in 10 measures, 5 calculation groups, Multiple reports, and multiple semantic models. Maintenance becomes painful quickly. And this is where User-Defined Functions (UDFs) prove to be very effective.

 

What Are DAX UDFs?

A DAX UDF allows you to define reusable logic:

DEFINE FUNCTION FunctionName = ( parameters ) => expression

 

Parameter Types in DAX UDFs

DAX UDFs support:

  • 0 to 256 parameters

Each parameter can optionally include type hints.

Type hints follow this structure:

[type] [subtype] [parameterMode]

 

Two Major Parameter Families

There are two major parameter families in DAX UDFs:

  1. Value Types
  2. Expression Types

 

1. Value Types are known as eager evaluation, as they evaluate in function calls. Supported types include AnyVal, Scalar, and Table.

AnyVal — The Default Parameter Type

If no type is specified, DAX assumes: AnyVal VAL

Example:

 

DEFINE FUNCTION
Streaks.GetLastRunGroup = ( userId, lastActivityDate ) =>
    CALCULATE (
        MAX ( user_dailyactivitydate[run_group] ),
        FILTER (
            ALL ( user_dailyactivitydate ),
            user_dailyactivitydate[userid] = userId
                && user_dailyactivitydate[ActivityDate] = lastActivityDate
        )
    )

EVALUATE
ROW ( "Last Run Group", Streaks.GetLastRunGroup ( 101, DATE ( 2026, 5, 6 ) ) )

 

Here, both parameters are implicit:

AnyVal VAL

Which means they accept numbers, text, dates, and booleans without strict validation. Without type hints, both parameters default to AnyVal — they accept any scalar and are evaluated eagerly at call time.

 

Scalar Parameters

Scalar parameters explicitly accept scalar values.

Example:

 

DEFINE FUNCTION
Streaks.GetLastActivityDate = ( userId ) =>
    CALCULATE (
        MAX ( user_dailyactivitydate[ActivityDate] ),
        FILTER (
            ALL ( user_dailyactivitydate ),
            user_dailyactivitydate[userid] = userId
        )
    )
EVALUATE
ROW ( "Last Activity Date", Streaks.GetLastActivityDate ( 101 ) )

 

This DAX code returns the latest activity date for a user.

 

Table Parameters

Table parameters accept entire tables.

This process controls when the evaluation happens.

VAL vs EXPR

 

t : TABLE VAL

means:

  • The caller evaluates the table first
  • The function receives a snapshot of that result

This process is an eager evaluation.

 

When working with a table, choosing between VAL and EXPR affects the filter context. With TABLE VAL, the caller evaluates the table and provides a snapshot. In contrast, TABLE EXPR gives the function a reference to the table, allowing it to apply its own filter context using CALCULATETABLE.

 

 

TABLE EXPR Example

 

DEFINE FUNCTION
Streaks.WindowActiveExpr = ( t : TABLE EXPR, days : NUMERIC ) =>
    COUNTROWS (
        CALCULATETABLE (
            t,
            DATESINPERIOD ( 'Date'[Date], TODAY (), -days, DAY )
        )
    )

EVALUATE
ADDCOLUMNS (
    VALUES ( user_dailyactivitydate[userid] ),
    "Active Days Last 7",  Streaks.WindowActiveExpr ( user_dailyactivitydate, 7  ),
    "Active Days Last 30", Streaks.WindowActiveExpr ( user_dailyactivitydate, 30 ),
    "Active Days Last 90", Streaks.WindowActiveExpr ( user_dailyactivitydate, 90 )
)
ORDER BY user_dailyactivitydate[userid]

 

 

 

Subtypes in DAX UDFs

Subtypes allow stricter scalar validation.

Supported subtypes include:

  • Variant
  • Int64
  • Decimal
  • Double
  • String
  • DateTime
  • Boolean
  • Numeric

 

Using Int64 and DateTime

Now let’s rewrite the earlier function using strict subtypes.

 

DEFINE FUNCTION
Streaks.GetLastRunGroup = ( userId : INT64, lastActivityDate : DATETIME ) =>
    CALCULATE (
        MAX ( user_dailyactivitydate[run_group] ),
        FILTER (
            ALL ( user_dailyactivitydate ),
            user_dailyactivitydate[userid] = userId
                && user_dailyactivitydate[ActivityDate] = lastActivityDate
        )
    )

EVALUATE
ROW ( "lastrungroup", Streaks.GetLastRunGroup ( 101, "2026-05-06" ) )

 

Now DAX validates arguments at call time. Subtypes let DAX validate arguments at call time instead of failing silently at runtime. Annotating userId as INT64 and lastActivityDate as DATETIME allows for immediate detection of incorrect argument types.

 

 

DAX UDFs are a major evolution in the DAX language.

DAX User-Defined Functions (UDFs) signify a significant change in how semantic models are structured. The streak example illustrates the progression clearly: start with no type hints for quick, flexible helpers; add subtypes (INT64, DATETIME) where type safety matters; and choose EXPR over VAL whenever the function needs to own the filter context.

Remember that:

- val is eager evaluation with a snapshot input.

- expr is lazy evaluation where the function determines context.

 

This distinction influences much of the UDF design in production Fabric semantic models.

 

The future of DAX is not just about writing measures but about creating reusable semantic logic systems.

Comments