DAX Accepts functions
The big update this month: business logic is written once, gets a signature and types, and is called from anywhere. Also — DAX Query View, report updates, and the list of known bugs with workarounds.
How the lesson works
The monthly updates module: what's new in Power BI, why Microsoft built it, and how to actually use it.
Five minutes of reading that give you the full picture, before diving deeper. Version 2.155.756.0.
- User-defined functions in DAX, now generally available. Business logic is written once as a function with a signature, types and optional parameters, and is called from a measure, a calculated column and a visual calculation. Requires compatibility level 1702.
- A new type system with two passing modes. The distinction between val toexpr determines which contexts the function inherits — and this is the most important technical point in the update.
- Governance warning: Object-level security doesn't propagate to functions. A function that references a protected measure isn't automatically protected.
- Sorting and filtering in the results grid of DAX Query View. The grid isn't editable, and filtering only works on results that have already been returned — which may be truncated.
- Report updates that eliminate manual maintenance: A date picker for a slicer that rolls forward with each refresh, a tooltip in sentence mode without a DAX measure, and conditional column width.
- A change that affects existing reports: The selection tools in Azure Maps are now enabled by default.
- 11 known bugs and limitations documented in Chapter V, with a workaround for each.
Navigating between topics
the buttons Previous Fnext fixed at the top of the screen. Also available via keyboard shortcut, by swiping on a phone, or by jumping to a topic from the topics list.
Questions and practice
At the end of the lesson, a short quiz with immediate feedback for each question, and application exercises with full solutions.
Progress is saved
The topic where you stopped and the topics you have read are saved only in your browser. You can close and come back, and reset from the topics list.
How each topic is built
Every topic opens with an explanation why The feature exists — the pain point in the field and the vision behind it — and moves on to the technical detail with code and examples. At the end of the lesson: a whole section of known bugs with workarounds, a short quiz, and a summary.
What the lesson covers
June 2026 update, version 2.155.756.0.
User-defined functions
The big feature of the month: DAX gets functions with a signature, types, and optional parameters.
DAX Query View
Sorting and filtering the results grid — and one trap you must know.
Report experience updates
A date picker for the slicer, tooltip in sentence mode, conditional column width, and mobile adaptation.
Bugs and workarounds
What's currently broken, why, and what to do until it's fixed.
The pain that gave birth to the feature
Before the syntax — the problem. Without it, it's hard to understand why this is the big announcement of the month.
In a mature enterprise model there are hundreds of measures, and within them the same business logic repeats over and over: the definition of an active customer, the conversion rate, the revenue recognition rule, the commission formula. When the rule changes — and business rules do change — someone has to find every place it's coded into. In practice they find eighty percent of them. The remaining twenty percent turn into reports that contradict each other, and that's exactly the moment management stops trusting the BI.
What was missing
DAX had one unit of reuse: the measure. But a measure isn't a function — you can't pass it parameters.
The improvised solution
Calculation groups, which pass parameters through the filter context. It works, but it's indirect, hard to debug, and expensive when the same logic is called multiple times in a query.
the vision
The semantic model as a software product: a function is a first-class object, saved as text, goes into Git, and goes through a code review.
What changed in general availability
The feature moved out of preview — and with it came threshold requirements you need to know.
| what | details |
|---|---|
| status | General availability in Power BI Desktop and the service starting with the June 2026 release |
| Compatibility level | 1702 and above. A model that wasn't upgraded simply won't keep the function |
| Where do you write | DAX Query View, TMDL View, and the formula bar of Model Explorer |
| Where does it live | junction Functions In Model Explorer, and in a PBIP project in the file functions.tmdl |
| How many parameters | From zero to 256 |
The first function
Three rows, and you already understand the whole structure.
/// Adds VAT to the received amount
FUNCTION AddVat =
( amount : NUMERIC ) =>
amount * 1.18
EVALUATE
{ AddVat ( 100 ) }
// returns 118
The keyword FUNCTION · name · parameter list · arrow · function body
- Written in DAX Query ViewUnder DEFINE, exactly like defining a measure in a query.
- Run and checkAn EVALUATE block with a function call returns the result immediately.
- Saving to the modelbutton Update model with changes Keeps all the functions in the query, or the row Update model: Add new function Above a single definition.
Documentation and naming conventions
A function no one understands how to call is a function no one will use.
/// Safely computes a profitability percentage, guarding against division by zero
/// @param {NUMERIC} revenue - the revenue amount
/// @param {NUMERIC} [cost] - cost, default 0
/// @returns profitability percentage as a decimal number
FUNCTION Fin.MarginPct =
( revenue : NUMERIC, cost : NUMERIC = 0 ) =>
DIVIDE ( revenue - cost, revenue )
EVALUATE
{ Fin.MarginPct ( 1000, 400 ) } // 0.6
| rule | What's allowed and what's not |
|---|---|
| uniqueness | A unique name in the model, that doesn't clash with a built-in function or a reserved word like measure or function |
| points | are allowed for namespaces like Fin.MarginPct — but not at the start, not at the end, and not consecutively |
| characters | Besides periods: alphanumeric characters and underscore only. No spaces |
| Parameter names | Points are completely forbidden, and you may not use a reserved word |
Where the function is called
After saving to the model, the function behaves like any built-in DAX function.
Total Sales with VAT = AddVat ( [Total Sales] )
-- a computed column (note the type conversion)
Sales Amount with VAT =
CONVERT ( AddVat ( Sales[Sales Amount] ), CURRENCY )
-- visual calculation
Sales Amount with VAT = AddVat ( [Sales Amount] )
DEFINE
FUNCTION AddVat = ( amount : NUMERIC ) => amount * 1.18
FUNCTION AddVatAfterDiscount =
( amount : NUMERIC, discount : NUMERIC ) =>
AddVat ( amount - discount )
EVALUATE
{ AddVatAfterDiscount ( 100, 20 ) } // 94.4
Why DAX needs types
The conceptual shift of the update: moving the failure from the report to write time.
In classic DAX, a measure that receives unexpected input silently returns a wrong result. In a one-person model that's tolerable. In a function library a whole team consumes, unexpected input is a bug that shows up in a management report.
Reliable IntelliSense
Whoever calls the function sees what it expects to receive, before running it.
Preventing a type error
You can't pass a column instead of a measure when the signature requires a measure.
Controlled conversion
Sub-typing forces a predictable conversion, instead of an incidental conversion by the engine.
( <Param> : [<Type>] [<Subtype>] [<PassingMode>] [= <Default>], ... )
=> <Body>
The types and the two families
The distinction that determines all the behavior: value vs. expression.
| family | type | what receives | When it's evaluated |
|---|---|---|---|
| Value types | AnyVal | A scalar or a table. The default | Immediately, before entering the function |
| Value types | Scalar | A scalar value, with an optional subtype | immediately |
| Value types | Table | table | immediately |
| Expression types | AnyRef | Any reference or expression | Inside the function body |
| Expression types | ColumnRef | Reference to a column | Inside the function body |
| Expression types | MeasureRef | Reference to the measure | Inside the function body |
| Expression types | TableRef | Reference to the table | Inside the function body |
| Expression types | CalendarRef | Reference to a calendar | Inside the function body |
Subtypes for scalar
FUNCTION CastToInt = ( x : INT64 ) => x
EVALUATE
{ CastToInt ( 3.4 ), CastToInt ( 3.5 ), CastToInt ( "5" ) }
// 3, 4, 5 — a non-numeric string will cause an error
Specifying a subtype makes the word Scalar redundant — and forces a predictable conversion
val vs expr
two functions with completely identical bodies, and two different results. This is the most important topic in the lesson.
/// Table as value: receives a table that's already been computed
FUNCTION CountRowsNow = ( t : TABLE VAL ) =>
COUNTROWS ( CALCULATETABLE ( t, ALL ( 'Date' ) ) )
/// Table as expression: accepts an uncalculated expression
FUNCTION CountRowsLater = ( t : TABLE EXPR ) =>
COUNTROWS ( CALCULATETABLE ( t, ALL ( 'Date' ) ) )
EVALUATE
{
CALCULATE ( CountRowsNow ( Sales ), 'Date'[Fiscal Year] = "FY2020" ),
CALCULATE ( CountRowsLater ( Sales ), 'Date'[Fiscal Year] = "FY2020" )
}
| the function | What it returns | why |
|---|---|---|
| CountRowsNow | Only FY2020 rows | The table was already filtered before entering the function, so the ALL inside no longer changes anything |
| CountRowsLater | all the years | The function received an uncomputed expression and evaluated it itself under ALL, meaning it removed the external filtering |
Optional parameters
A default in the signature makes the parameter optional — with four rules you need to know.
FUNCTION AddNum = ( x : NUMERIC = 1, y : NUMERIC = 2 ) => x + y
EVALUATE
{ AddNum (), AddNum ( 1 ), AddNum ( , 2 ), AddNum ( 5, 5 ) }
// 3, 3, 3, 10
An empty comma skips an argument and uses the default
| the rule | What does this mean |
|---|---|
| location | An optional parameter can appear anywhere. But the minimum number of arguments is determined by the parameter The rightmost required — in a function with three parameters where only the second is optional, you still must pass three |
| the context | Inheritance of row context and filter context is determined by the passing mode of that parameter |
| Field of vision | The default expression only sees names visible in the location the setting, not at the call site. It also can't refer to another optional parameter |
| Type check | The type hint is enforced on the default only when it's actually used. If the caller supplied an argument, the check runs against that instead |
Runtime type checking
When the function needs to accept more than one input type — check inside the function body.
/// returns a currency name by numeric key or by text code
FUNCTION Dim.GetCurrencyName = ( currency ) =>
IF (
ISINT64 ( currency ),
LOOKUPVALUE ( 'Currency'[Currency], 'Currency'[CurrencyKey], currency ),
LOOKUPVALUE ( 'Currency'[Currency], 'Currency'[Code], currency )
)
EVALUATE
{ Dim.GetCurrencyName ( 36 ), Dim.GetCurrencyName ( "USD" ) }
TABLEOF and NAMEOF
Two new helper functions that let you write truly generic functions.
| function | What it returns |
|---|---|
| TABLEOF | the table that a column, measure, or calendar belongs to |
| NAMEOF | the name of a table, column, measure or calendar — as text |
/// Returns the most common value of an expression over the table it belongs to
FUNCTION Stat.ModeX = ( e : ANYREF ) =>
VAR newTable = ADDCOLUMNS ( TABLEOF ( e ), "expr", e )
VAR freqTable = GROUPBY ( newTable, [expr], "count", SUMX ( CURRENTGROUP (), 1 ) )
VAR maxCount = MAXX ( freqTable, [count] )
RETURN
SELECTCOLUMNS ( FILTER ( freqTable, [count] = maxCount ), "expr", [expr] )
EVALUATE Stat.ModeX ( [Total Sales] )
The function doesn't hard-code a table name — TABLEOF finds it from the expression received
Governance limitations and risks
What you must check before a function enters an enterprise model.
| limitation | The practical implication |
|---|---|
| Unqualified names | A name without an explicit table prefix is treated as a measure reference, so it isn't included in dependency tracking. Always 'Sales'[Amount] and never [Amount] |
| No recursion | Neither directly nor mutually |
| No overload | Two functions can't share a name with different signatures |
| There is no explicit return type | Need to ensure consistency within the function body itself |
| No view folders | and you also can't hide or expose a function |
| No combination with translations | Translated model — the functions stay out |
| No enum return | A built-in function expecting an enum won't accept a UDF |
EVALUATE INFO.USERDEFINEDFUNCTIONS () // Full metadata, requires write permission
EVALUATE INFO.FUNCTIONS ( "ORIGIN", "2" ) // names only, no write permission
UDF or a calculation group
Two capabilities that look similar and solve completely different problems.
| when | the tool | why |
|---|---|---|
| Business logic with variable parameters | UDF | Passing parameters through an explicit signature saves the overhead of passing them through the filter context, especially when the same logic is called several times in one query |
| Display variations across all the measures | Calculation group | Prior year, year-to-date cumulative, percent of total — something that needs to apply to a hundred measures without rewriting them |
| combination | both | A calculation group that internally calls a shared function — this is the pattern recommended for large models |
Known Bugs and Workarounds
What is currently broken, per the bug entries in the official docs and community reports — and what to do until it is fixed.
| the phenomenon | the reason | The workaround |
|---|---|---|
| Red underlines and validation errors when passing a column as a parameter expr | A known inconsistency of the parser in advanced scenarios | run it and check the actual result; write full qualified references; split into a simpler helper function |
| Type hint ColumnRef or MeasureRef Deferred at the call site | Known bug: the specific reference types aren't accepted at every call site | to drop toAnyRef and perform validation in the function body |
| A custom error message with ERROR not displayed | DAX generates an internal error from incorrect parameter use before the validation code runs | Don't rely on custom messages at this stage; document the requirement in/// |
| Renaming a column breaks a function | An unqualified name is treated explicitly as a reference to a measure and is therefore not included in dependency tracking | Always use the full table name inside a function |
| The function refuses to save | a compatibility level below 1702, or a model with no tables | to upgrade the compatibility level; in TMDL View an automatic prompt is shown |
| There's no IntelliSense in visual calculations, in TMDL, or in SSMS | Partial support at this stage | Write and test it in DAX Query View and then copy |
| A measure in a report with live connect can't see a function from the source model | A report-level measure can reference it but without IntelliSense; a model-level measure in a complex model cannot at all | Define the logic in the source model as a measure too, not only as a function |
| A calculated column returns an inconsistent type | A function that returns different types depending on the branch chosen | to wrap inCONVERT or split into two functions with a uniform type |
| Saved DAX queries didn't go into Git | Setting up an initial Git integration from the workspace doesn't include them | To save critical queries as text files in the repository at the same time |
| Maps started behaving differently in an old report | The selection tools in Azure Maps are now on by default, even in existing reports | To turn off the switch Selection In the Controls section |
| Date picker doesn't appear for a teammate | A preview feature that's enabled per device individually | to make sure everyone has enabled the preview, and not to rely on it in a production report |
Sorting and filtering in the results grid
A small interface improvement that saves run cycles — with one big trap.
Why Microsoft introduced this
Debugging DAX is a loop: run, look, change, run again. Until now, every small question about the result — what's the highest value, are there negative rows — required rewriting the query and running it again. Each round costs time as well as capacity resources.
What was actually added
Every column header shows a sort icon to toggle between ascending and descending order on click. The three-dot menu adds clearing the sort, searching values, and filtering by value selection.
| the limitation | the value | What does this mean in practice |
|---|---|---|
| Volume per query | 15MB | On an exception the current row ends and no further rows are written |
| Values for the query | 1,000,000 | With 20 columns you get at most 50,000 rows |
| Rows in the browser | 99,999 | and write permission on the semantic model is required |
| Display format | not shown | Currency and thousands separators won't appear in the grid |
EVALUATE
FILTER (
SUMMARIZECOLUMNS (
'Product'[Category],
'Date'[Month of Year],
"Sales", [Total Sales]
),
[Sales] > 100000
)
ORDER BY [Sales] DESC
Report experience updates
The common thread: eliminating repetitive manual work.
Why Microsoft introduced this
Almost every change here erases maintenance: a date slicer that needs updating every month, a text measure written only so a tooltip is readable, manually tuning axis ranges so a bar isn't cut off. At the same time, turning on the map selection tool by default teaches a second principle: a feature that exists but is hidden behind a switch that's off is a feature that doesn't exist.
Date picker for the slicer
A relative selection that rolls forward with every refresh. Anchored to today, to the first date, or to the last date in the data — and that's what prevents a report from breaking when the ETL is delayed. In preview.
Auto-expansion in the matrix
Defined separately for rows and columns. The big value is in Personalize this visual: a field the reader adds opens up immediately.
Tooltip in sentence mode
A free-form sentence that refers to the tooltip fields — without a dedicated DAX measure. Saves unnecessary text measures in the model.
Conditional column width
Turn on Custom widths and click fx. The interesting pattern: tying the width to a What-If parameter with a slider.
Azure Maps
The selection tools are lit by default — circle, rectangle, polygon, travel time and distance. Close a polygon with the C key.
Shape map
in general availability, with uploading TopoJSON and GeoJSON, and re-downloading a map you uploaded.
Adapting the report for mobile
The mobile layout isn't a shrunken desktop — it's a different report.
- Building both layouts togetherDuplicating a page duplicates both layouts, so a page with a header and navigation serves as a template for the rest of the pages.
- Select only what mattersRecommended minimum size: 323 points wide for a large visual, 158 for a small one. Font no smaller than nine points.
- Clean up noiseRemove axis titles, gridlines and legend; arrange navigation and slicers horizontally; reduce white space.
- Testing an interactionTooltips are disabled on the editing canvas but work in the app. Slicer selections on the canvas don't carry over to the grid layout.
| The new update | What it gives on a small screen |
|---|---|
| Date picker | A relative selection saves typing, and is preferable to two separate slicers for a range |
| Conditional column width | The same table in two different widths for desktop and mobile, without duplicating the visual |
| Tooltip in sentence mode | One readable sentence instead of a field list that gets cut off on a narrow screen |
Quiz — 12 questions
One question at a time. A correct answer is colored green, an incorrect one red, and the correct answer is always revealed along with a full technical explanation.
In expr mode, which context does a parameter inherit?
What happens when a function refers to a measure protected by object-level security?
Why should you not rely on filtering in the DAX Query View results grid to validate data?
The MeasureRef type hint is rejected at the call site. What's the solution?
What's the minimum compatibility level required for user-defined functions?
In a function with three parameters where only the second is optional — how many arguments must the caller supply?
What's special about a comment that starts with three slashes?
What will happen if you use an unqualified reference in the style [Amount] inside a function when you mean a column?
Where is the mobile layout you built for the report displayed?
When do you choose a UDF instead of a calculation group?
A function defined with ( t : TABLE ) and called inside CALCULATE with a year filter — what will it return?
You haven't answered questions yet — 0 out of 12
Summary and next steps
Ten rows that sum up the entire lesson.
| topic | what to remember |
|---|---|
| the rationale | Business logic becomes a code-managed library, not copy-paste between measures |
| definition | FUNCTION Name = ( p : TYPE SUBTYPE MODE = default ) => body with documentation in-/// |
| val vs expr | val at the caller inherits row and filter; expr inside inherits filter only |
| Reference types | ColumnRef, MeasureRef, TableRef, CalendarRef — always expr, without type conversion |
| Optional | The minimum argument count is determined by the rightmost required parameter |
| Main risk | OLS doesn't carry over to functions |
| golden rule | Always fully qualified names inside a function |
| DAX Query View | Sorting and filtering only, and only on results already returned |
| mobile | Build the mobile layout together with the desktop one, not after it |
| source of truth | functions.tmdl In a PBIP project, in a workspace connected to Git |
You can also navigate with the buttons at the top of the screen, keyboard arrows, and on phone with a swipe