General Availability · June 2026 Release

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.

Quick Answer

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.

📅 Last verified: June 2026 ⏱ ~17 min read ✍️ A.J., Data Engineering Researcher 🔗 Source: Microsoft Learn

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.

What Changed at General Availability
  • 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.8

Notice 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 UDFsBest For
DAX Query View (DQV)More code assistance, autocomplete, syntax highlighting, Quick queries menu
TMDL ViewDrag-and-drop functions onto canvas, Script TMDL, ideal for copying functions between models
Model ExplorerCreate/edit via formula bar — new at GA, fastest for simple edits
📌
Naming Convention — Pascal Case

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.

ModeEvaluation TimingBehaves LikeUse When
VAL (eager)Evaluated once, immediately, in the caller’s context — before entering the functionA fixed variableStable, pre-resolved values — numbers, text, pre-aggregated results
EXPR (lazy)Passed as an unevaluated expression — resolved inside the function body, every time it’s referencedA living formula that responds to contextContext-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 )
Field note — A.J., UIG Data Lab

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 TypeForces EXPR?Automatic Context Transition?
MEASUREREFYes — always EXPRYes — guaranteed
ANYREFYes — always EXPRNo — must wrap in CALCULATE manually
COLUMNREFYes — always EXPRNo — not applicable to columns
TABLEREFYes — always EXPRNo — not applicable to tables
SCALAR … EXPRExplicitly declaredNo — 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 ) )
⚠️
Naming Convention for ANYREF Parameters

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)

TypeAccepts
AnyValA scalar or a table — the default if you omit type entirely
ScalarA scalar value; can add a subtype (Int64, Decimal, Double, String, DateTime, Boolean, Numeric, Variant)
TableA table expression

Expression / Reference Types (Lazy — always EXPR)

TypeAcceptsContext Transition
AnyRefAny reference or expression — most permissiveManual (wrap in CALCULATE)
MeasureRefOnly an actual measure referenceAutomatic
ColumnRefOnly a column from a model tableN/A — use with TABLEOF
TableRefOnly a table referenceN/A
CalendarRefOnly a reference to a calendar tableN/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 subtype

TABLEOF 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.

📌
Optional Parameters — New at GA

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.

Field note — A.J., UIG Data Lab

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.

CapabilityCalculation GroupsUDFs
Applies to “whichever measure is selected”Yes — via SELECTEDMEASURE()No — must be called explicitly
Accepts parametersNoYes — up to 12
Items can be grouped for mutual exclusivityYesNo
Callable directly like a built-in functionNoYes — from anywhere DAX is supported
Best forTime intelligence and unit-conversion patterns applied uniformly across many measuresEncapsulating 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()
📌
Self-Documenting Functions with /// Comments

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.

🏛️
Building an Internal Function Library

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

LimitationDetail
Compatibility level requirementRequires database compatibility level 1702 or higher. May upgrade automatically, or set manually in Tabular Editor.
Cannot return enum valuesBuilt-in functions that accept enum-typed parameters cannot use a UDF in that argument position.
Unbound EXPR parameters not evaluatedAn 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 contextsReport-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 IntelliSenseVisual 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 usageCertain advanced UDF usage patterns can produce parser inconsistencies — test thoroughly, particularly with deeply nested function calls.
Cannot be groupedUnlike 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

DAX User-Defined Functions (UDFs) let you define reusable, parameterized DAX logic once and call it from measures, calculated columns, visual calculations, and other UDFs. They are first-class model objects, viewable under the Functions node in Model Explorer, and are authored using a FUNCTION keyword with an arrow (=>) separating the parameter list from the function body. DAX UDFs reached general availability in Power BI Desktop and the Power BI Service with the June 2026 release, after entering preview in September 2025.
VAL is eager evaluation — the argument is evaluated once, in the caller’s context, before being passed into the function, and behaves like a fixed variable. EXPR is lazy evaluation — the argument is passed as an unevaluated expression and is evaluated inside the function body, every time it is referenced, respecting any context changes such as CALCULATE applied within the function. EXPR is required for parameters that need to support context transition, such as measure references used inside iterators, and is essential for context-sensitive logic like dynamic time intelligence.
No, except for MEASUREREF. A parameter declared as ANYREF or EXPR does not automatically receive context transition inside an iterator unless the caller passed a measure reference. To guarantee correct behavior regardless of what expression the caller provides, wrap the parameter reference in an explicit CALCULATE inside the function body. MEASUREREF is the only reference type that guarantees automatic context transition, because it restricts the parameter to an actual measure.
DAX User-Defined Functions require database compatibility level 1702 or higher. Power BI Desktop and the Power BI Service may upgrade this automatically when a UDF is created, but it can also be set manually in tools such as Tabular Editor.
Calculation groups apply a set of calculation items uniformly across whichever measure is currently selected, using SELECTEDMEASURE(), and calculation items can be grouped together for mutual exclusivity. UDFs cannot be grouped and calculation items cannot take parameters, while UDFs accept up to 12 typed parameters and can be called directly like any other DAX function from anywhere DAX is supported. In practice, many GA-era models use a UDF to encapsulate the reusable logic and call that UDF from inside a calculation item, combining the parameterization of a UDF with the selected-measure behavior of a calculation group.
daxlib.org is a free, open-source, model-independent repository of DAX user-defined functions that can be imported and used in any semantic model. It is maintained by the DAX community and referenced directly by Microsoft documentation as a source of reusable, tested function logic.
⚠ Accuracy Disclaimer

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.

A.J. Data Engineering Researcher & Technical Writer · UIG Data LabAll articles →
A.J. researches and writes about data engineering, analytics architecture, Microsoft Fabric, and modern cloud data platforms. Coverage spans Microsoft Fabric, Power BI, Azure Data Engineering, Databricks, Snowflake, Apache Spark, dbt, Apache Airflow, and modern cloud data infrastructure. The focus is practitioner-level content that helps data professionals understand platform capabilities, evaluate technology decisions, optimize costs, and implement practical solutions using official documentation, product updates, community insights, and industry best practices.
DAXPower BIUser-Defined FunctionsSemantic ModelsCalculation GroupsTMDLMicrosoft FabricTime Intelligence
Scroll to Top