DAX User-Defined Functions — A Complete Guide for Power BI
DAX User-Defined Functions (UDFs) let you write a calculation once and call it from anywhere DAX is supported — measures, calculated columns, visual calculations, and other functions. This guide covers the full DEFINE FUNCTION syntax, the val/expr parameter modes that trip up most developers, context transition mechanics, calculation group migration, debugging with DAX Query View, and daxlib.org — verified against official Microsoft Learn documentation through June 2026.
DAX User-Defined Functions reached general availability in Power BI Desktop and the Power BI Service with the June 2026 release, after entering preview in September 2025. A UDF is declared with FUNCTION Name = (parameters) => body using arrow notation. The most important concept is parameter passing mode: val (eager evaluation) resolves the argument once before the function runs and behaves like a fixed variable; expr (lazy evaluation) passes an unevaluated expression that is resolved inside the function body, which is required for context-sensitive logic like dynamic time intelligence. UDFs require database compatibility level 1702 or higher and are stored as functions.tmdl in Power BI Projects, making them fully Git-trackable.
What Are DAX User-Defined Functions?
DAX User-Defined Functions (UDFs) let you define a parameterized calculation once and reuse it across measures, calculated columns, visual calculations, and other UDFs — instead of copying the same expression into dozens of places and hoping they stay in sync. A UDF is a first-class model object, viewable under the Functions node in Model Explorer, alongside tables, measures, and relationships.
DAX previously offered no equivalent — Power Query had M-language custom functions for years, but DAX itself only gained this capability with the September 2025 preview release. UDFs reached general availability in Power BI Desktop and the Power BI Service with the June 2026 release, after months of community testing validated the feature for production use.
- Web modeling support — view and edit UDFs in the browser with the same convenience as Desktop
- Optional parameters — assign a default expression so callers can omit arguments
- Enhanced type hints — greater flexibility and runtime type safety
- Model View authoring — create and edit UDFs directly in Model View, not just DAX Query View or TMDL view
Because UDFs are first-class model objects with typed signatures and documentation, tools like Copilot can discover and invoke them more effectively — giving AI agents a trusted, well-defined API into your business logic rather than relying on inferred DAX expressions scattered across measures.
DAX UDF Syntax — DEFINE FUNCTION and Arrow Notation
A function is declared using the FUNCTION keyword, a parameter list in parentheses, and an arrow (=>) — sometimes called a “rocket” in other programming languages — separating the signature from the function body.
FUNCTION <FunctionName> = (
[<ParameterName> [: [<ParameterType>] [<ParameterSubtype>] [<ParameterPassingMode>]] [= <DefaultExpression>], ...]
) => <FunctionBody>A Working Example
DEFINE
/// AddTax takes in amount and returns amount including tax
FUNCTION AddTax = ( amount : NUMERIC ) => amount * 1.1
FUNCTION AddTaxAndDiscount = ( amount : NUMERIC, discount : NUMERIC ) =>
AddTax ( amount - discount )
EVALUATE { AddTaxAndDiscount ( 10, 2 ) }
// Returns 8.8Notice that AddTaxAndDiscount calls AddTax directly — UDFs can call other UDFs, allowing layered, modular calculations. Once saved to the model, you use a UDF exactly like a built-in DAX function, anywhere DAX is supported:
Sales Amount with Tax = CONVERT ( AddTax ( 'Sales'[Sales Amount] ), CURRENCY )
| Where to Author UDFs | Best For |
|---|---|
| DAX Query View (DQV) | More code assistance, autocomplete, syntax highlighting, Quick queries menu |
| TMDL View | Drag-and-drop functions onto canvas, Script TMDL, ideal for copying functions between models |
| Model Explorer | Create/edit via formula bar — new at GA, fastest for simple edits |
Use Pascal case (AddTax, not ADDTAX or addtax) for UDF names. This visually distinguishes user-defined functions from built-in DAX functions, which always use uppercase. Names can include periods for namespacing — for example Finance.Margin or Supply.LeadTime — useful in shared function libraries where multiple teams contribute functions to one model.
val vs expr — Eager vs Lazy Evaluation, Explained
This is the single most misunderstood concept in DAX UDFs, and the most common source of silently wrong results. Every parameter has a passing mode that controls when and how it is evaluated.
| Mode | Evaluation Timing | Behaves Like | Use When |
|---|---|---|---|
| VAL (eager) | Evaluated once, immediately, in the caller’s context — before entering the function | A fixed variable | Stable, pre-resolved values — numbers, text, pre-aggregated results |
| EXPR (lazy) | Passed as an unevaluated expression — resolved inside the function body, every time it’s referenced | A living formula that responds to context | Context-sensitive logic — measures inside iterators, dynamic filters, time intelligence |
Why This Matters — A Failing Example
Consider a function meant to filter sales to specific billing document types:
FUNCTION FilterDocTypes = ( docTypeExpr : STRING ) =>
CALCULATE ( [Sales Amount], 'Sales'[Document Type] = docTypeExpr )Without a type hint, the parameter defaults to ANYVAL with passing mode VAL. The parameter arrives as a fixed value — it cannot further modify filter context inside the function. The CALCULATE effectively does nothing useful here because the value was already resolved before the function ran. To make context-sensitive logic actually work, change the parameter to use EXPR:
FUNCTION FilterDocTypes = ( docTypeExpr : SCALAR STRING EXPR ) =>
CALCULATE ( [Sales Amount], 'Sales'[Document Type] = docTypeExpr )Think of VAL as “DAX computes this first, then hands you the answer” and EXPR as “DAX hands you the question, and your function decides when to ask it.” If your function needs to change filter context — wrap something in CALCULATE, iterate with a different filter, or respond to row context — the parameter touching that logic almost always needs EXPR. If you’re just passing a number or a stable text value, VAL is simpler and works fine.
Context Transition Inside UDFs — The Part Everyone Gets Wrong
Even with EXPR set correctly, there is a second trap: EXPR parameters do not automatically receive context transition inside an iterator, unlike a direct measure reference. This is the detail that separates a function written by a beginner from one written by someone who understands DAX deeply.
| Reference Type | Forces EXPR? | Automatic Context Transition? |
|---|---|---|
| MEASUREREF | Yes — always EXPR | Yes — guaranteed |
| ANYREF | Yes — always EXPR | No — must wrap in CALCULATE manually |
| COLUMNREF | Yes — always EXPR | No — not applicable to columns |
| TABLEREF | Yes — always EXPR | No — not applicable to tables |
| SCALAR … EXPR | Explicitly declared | No — must wrap in CALCULATE manually |
MEASUREREF is the only reference type that guarantees automatic context transition — because it restricts the parameter to an actual measure, and any measure reference implicitly invokes CALCULATE. With ANYREF, the caller could pass a measure, a column, or an arbitrary expression — the function author cannot assume context transition happens and must defensively wrap the parameter reference in an explicit CALCULATE.
// Defensive pattern — guarantees correct behavior regardless of what the caller passes
FUNCTION BestCustomers = ( metricExpr : ANYREF, n : INT64 ) =>
TOPN ( n, VALUES ( Customer[CustomerName] ), CALCULATE ( metricExpr ) )Because ANYREF accepts a measure, a column, a table, or an arbitrary calculation, IntelliSense cannot tell the caller what is expected. The community-recommended convention is suffixing ANYREF parameter names with Expr — for example amountExpr or targetExpr — so anyone reading a call site immediately understands the parameter is an unevaluated expression, not a resolved value.
Type Hints — Type, Subtype, and Parameter Mode
Type hints are optional but strongly recommended. They follow the form ParameterName : Type Subtype ParameterMode and provide IntelliSense guidance, runtime type safety, and self-documenting call sites.
Value Types (Eager — VAL by default)
| Type | Accepts |
|---|---|
| AnyVal | A scalar or a table — the default if you omit type entirely |
| Scalar | A scalar value; can add a subtype (Int64, Decimal, Double, String, DateTime, Boolean, Numeric, Variant) |
| Table | A table expression |
Expression / Reference Types (Lazy — always EXPR)
| Type | Accepts | Context Transition |
|---|---|---|
| AnyRef | Any reference or expression — most permissive | Manual (wrap in CALCULATE) |
| MeasureRef | Only an actual measure reference | Automatic |
| ColumnRef | Only a column from a model table | N/A — use with TABLEOF |
| TableRef | Only a table reference | N/A |
| CalendarRef | Only a reference to a calendar table | N/A |
Type-Checking Helpers and Information Functions
Validate parameter types defensively inside a function body using built-in DAX type-checking functions, and use TABLEOF / NAMEOF with COLUMNREF or TABLEREF parameters:
FUNCTION SafeRatio = ( numerator : SCALAR DOUBLE, denominator : SCALAR DOUBLE ) =>
IF ( denominator = 0, BLANK(), numerator / denominator )
FUNCTION CastToInt = ( x : SCALAR INT64 VAL ) => x
EVALUATE { CastToInt ( 3.4 ), CastToInt ( 3.5 ), CastToInt ( "5" ) }
// Returns 3, 4, 5 — automatic coercion based on declared subtypeTABLEOF returns the full table associated with a given column, measure, or calendar reference — useful when a function needs to operate on “whatever table this column belongs to” without hardcoding a table name. NAMEOF returns the name of a table, column, measure, or calendar as a text string, useful for runtime validation messages.
Assign a default expression to a parameter using an equals sign: y : NUMERIC = 1. If the caller omits the argument, the default expression is used. Given FUNCTION AddNum = (a : NUMERIC = 0, b : NUMERIC = 0) => a + b, you can call AddNum(), AddNum(1), AddNum(,2), or AddNum(1,2) — the default respects type hints at runtime and can invoke other UDFs or built-in functions.
Dynamic Time Intelligence with UDFs
Dynamic time intelligence — letting a user pick “MTD,” “QTD,” or “YTD” from a slicer and having one measure respond correctly — is the canonical use case requiring EXPR. The function must evaluate its argument inside the current filter context at call time, not at the moment the function was defined.
DEFINE
FUNCTION GetPeriod = ( MI : ANYREF, periodType : SCALAR STRING ) =>
SWITCH (
TRUE(),
periodType = "MTD", CALCULATE ( MI, DATESMTD ( 'Date'[Date] ) ),
periodType = "QTD", CALCULATE ( MI, DATESQTD ( 'Date'[Date] ) ),
periodType = "YTD", CALCULATE ( MI, DATESYTD ( 'Date'[Date] ) ),
MI
)
EVALUATE
SUMMARIZECOLUMNS (
'Date'[Month],
"Period Value", GetPeriod ( [Sales Amount], "QTD" )
)MI uses ANYREF so it accepts any measure or expression dynamically, and the explicit CALCULATE wrapping ensures context transition happens regardless of what the caller passes — the defensive pattern from Section 04 applied directly to time intelligence.
This single function replaces what used to require three separate measures — one each for MTD, QTD, and YTD — multiplied across every base measure in the model. A model with 20 base measures needing all three time periods previously meant 60 nearly-identical measures. One GetPeriod function and a calculation group calling it covers all 60 combinations from a single definition.
Migrating Calculation Groups to UDFs — and When to Combine Them
UDFs and calculation groups solve overlapping but distinct problems, and the GA-era best practice is usually to combine them rather than replace one with the other.
| Capability | Calculation Groups | UDFs |
|---|---|---|
| Applies to “whichever measure is selected” | Yes — via SELECTEDMEASURE() | No — must be called explicitly |
| Accepts parameters | No | Yes — up to 12 |
| Items can be grouped for mutual exclusivity | Yes | No |
| Callable directly like a built-in function | No | Yes — from anywhere DAX is supported |
| Best for | Time intelligence and unit-conversion patterns applied uniformly across many measures | Encapsulating the reusable logic itself, with explicit parameters |
The Combined Pattern — UDF Called Inside a Calculation Item
Rather than choosing one or the other, define the reusable logic as a UDF, then call that UDF from inside a calculation item using SELECTEDMEASURE() as the argument:
// Calculation item inside a "Period" calculation group Period Value = VAR _type = SELECTEDVALUE ( Period[Type] ) RETURN GetPeriod ( SELECTEDMEASURE(), _type )
This gives report authors the calculation-group experience — pick a measure, apply a period from a slicer — while the actual logic lives in one centrally maintained, parameterized, testable UDF rather than being duplicated across calculation items.
Migrating a Wall of Formatting Measures
The other dominant migration pattern is collapsing hundreds of duplicated formatting-string measures into one function:
// Before: dozens of near-identical measures, one per KPI
Revenue Status Color = IF ( [Revenue] >= [Revenue Target], "#16a34a", "#dc2626" )
Margin Status Color = IF ( [Margin] >= [Margin Target], "#16a34a", "#dc2626" )
// ...repeated for every KPI in the model
// After: one function, called by every KPI
FUNCTION StatusColor = ( actual : SCALAR DOUBLE, target : SCALAR DOUBLE ) =>
IF ( actual >= target, "#16a34a", "#dc2626" )
Revenue Status Color = StatusColor ( [Revenue], [Revenue Target] )
Margin Status Color = StatusColor ( [Margin], [Margin Target] )A model with 150 dynamically colored measures previously meant editing 150 places when a threshold rule changed. With the function pattern, that becomes one edit, validated once, propagated everywhere it is called.
Debugging UDFs with EVALUATE in DAX Query View
Test a UDF locally before it touches a single measure or report visual, using EVALUATE directly in DAX Query View. This is the fastest feedback loop available — no need to build a measure, drop it on a visual, and inspect the result.
DEFINE
FUNCTION PercentChange = ( newValue : NUMERIC, oldValue : NUMERIC ) =>
DIVIDE ( newValue - oldValue, oldValue )
EVALUATE
{
PercentChange ( 120, 100 ),
PercentChange ( 80, 100 ),
PercentChange ( 0, 0 )
}
// Returns 0.2, -0.2, BLANK()Quick Queries in Model Explorer
Right-click a UDF under the Functions node in Model Explorer to access three Quick queries that generate boilerplate EVALUATE scripts automatically:
Evaluate
Generates an EVALUATE call against the existing function with placeholder arguments, ready to edit and run.
Define and Evaluate
Pulls the full function definition plus a test EVALUATE into the query window — ideal for editing logic and testing in one pass.
Define All Functions in This Model
Dumps every UDF definition currently in the model into the query window — useful for reviewing or auditing the full function library at once.
Inspecting UDFs Programmatically
Use INFO.USERDEFINEDFUNCTIONS() to query full metadata for every UDF in the model — names, descriptions, parameters, and type hints. This requires write permission and only returns functions actually saved to the model.
EVALUATE INFO.USERDEFINEDFUNCTIONS()
Place triple forward slashes (///) directly above a function definition, and optionally above each parameter using @param tags. These comments automatically surface inside IntelliSense — typing a function name shows its description and signature, and INFO.USERDEFINEDFUNCTIONS() becomes a living, searchable, auditable catalog of what’s deployed.
daxlib.org — A Community Library of Reusable Functions
daxlib.org is a free, open-source, model-independent repository of DAX user-defined functions, referenced directly by Microsoft documentation as a source of reusable, community-tested logic. Functions published there are written to be portable — not tied to any specific model’s table or column names — so they can be imported into any semantic model.
Rather than writing common patterns from scratch — percent change, weighted averages, ranking helpers, statistical functions — daxlib.org lets teams pull a tested implementation and adapt it, the same way a software engineer would reach for a well-maintained open-source package instead of reimplementing a sorting algorithm.
For enterprise teams, the recommended pattern is a Git-integrated shared function library model — a dedicated semantic model whose sole purpose is housing approved, namespaced UDFs (Finance.Margin, Supply.LeadTime, HR.Attrition). Other models reference or copy these functions via TMDL, giving the organization one governed source of truth for business logic instead of each team reinventing the same calculations independently.
Current Limitations of DAX UDFs
| Limitation | Detail |
|---|---|
| Compatibility level requirement | Requires database compatibility level 1702 or higher. May upgrade automatically, or set manually in Tabular Editor. |
| Cannot return enum values | Built-in functions that accept enum-typed parameters cannot use a UDF in that argument position. |
| Unbound EXPR parameters not evaluated | An EXPR-typed parameter that is never referenced in the function body is never evaluated — by design, but a common source of confusion when debugging. |
| No IntelliSense in some contexts | Report-level measures in live-connect reports can call source-model UDFs but without IntelliSense. Composite model-based measures cannot call source-model UDFs at all. |
| Limited tooling IntelliSense | Visual calculations formula bar, TMDL Extension for VS Code, and SQL Server Management Studio all have limited or no IntelliSense support for UDFs currently. |
| Parser inconsistencies in advanced usage | Certain advanced UDF usage patterns can produce parser inconsistencies — test thoroughly, particularly with deeply nested function calls. |
| Cannot be grouped | Unlike calculation items, UDFs have no native grouping or mutual-exclusivity mechanism — combine with calculation groups when that behavior is needed. |
FAQ — DAX User-Defined Functions
Official Resources — DAX UDF Documentation
Information is verified against official Microsoft Learn DAX UDF documentation and SQLBI through June 2026. DAX UDFs are newly GA as of June 2026 — some tooling integrations (IntelliSense in third-party editors, certain advanced usage patterns) remain in active development. UIG Data Lab is an independent publication, not affiliated with Microsoft Corporation or SQLBI.