This time we’re going bigger than ever. Fabric, Power BI, SQL, AI and more. We're covering it all. You won't want to miss it.
Learn moreGet Fabric Certified for FREE during AI Skills Fest. This week only. Secure your voucher now.
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:
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:
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 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:
This process is an eager evaluation.
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:
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 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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.