Oct. 21, 2026 · @atilayberber
As of June 2026, you can write your own functions in DAX. User-Defined Functions (UDFs) are now generally available in Power BI Desktop and Power BI Service and are enabled by default. In this post, I’ll walk you through what UDFs are, their syntax, parameter types, real-world examples from the retail industry, how they differ from calculation groups, and what to watch out for in production—step by step.
Introduction: The Hidden Cost of Copy-Paste DAX
The same story plays out in every growing Power BI model. The gross profit margin formula is first written once. Then, three more copies are created for store, category, and region reports. In one of them DIVIDE one / is used, while another selects a different cost column. Six months later, there are three different definitions of the same KPI, and no one knows which one is correct.
This problem is called KPI drift. Until now, there was no real solution for this in DAX. Calculation groups and base measures worked to some extent, but since they couldn’t accept parameters, they didn’t fit every situation.
UDFs fill this gap. You define a business rule once as a parameterized function. Then you call it in measures, calculated columns, visual calculations, and other functions just like a built-in DAX function. When the rule changes, you update just one place. The Tabular Editor team describes this feature as the biggest change to the DAX language in recent years; I don’t think that’s an exaggeration.
From Preview to General Availability: A Brief History
UDFs were released in preview with the September 2025 Power BI Desktop update and reached general availability (GA) with the June 2026 release. Microsoft made the announcement on June 2, 2026; in the Desktop release that came out on June 9 (2.155.756.0), the feature is now enabled by default without needing to be turned on in the settings menu.
| Timeline | What Happened |
|---|---|
| June 2026 | GA: optional parameters, modeling on the web, editing in Model view, enabled by default |
| June 2026 | Tabular Editor 3.26.2: GA syntax and type mismatch warnings |
| December 2025 | SQLBI announced the DAX Lib community function library |
| September 2025 | General preview in Power BI Desktop |
The most significant new features introduced with the GA compared to the preview are as follows:
- Optional parameters: You can
= <varsayılan ifade>to make it optional. - Web-based modeling: You can view and edit UDFs in the Power BI Service directly from your browser.
- Model view: You can create one in Desktop by right-clicking > New Function and edit it in the formula bar.
- Dependency tracking: When you rename a table, column, or measure, the function expressions in the model are automatically updated.
- Catalog:
INFO.USERDEFINEDFUNCTIONS()lists all UDFs in the model with details. - SSMS support: You can script and create UDFs in SQL Server Management Studio 22.5 and later (syntax highlighting is not yet available).
This feature requires a database compatibility level of 1702 or higher. Microsoft employees have also indicated on the community forum that UDFs are now available in Azure Analysis Services; I recommend verifying this on your own server before using them in an enterprise AAS environment.
What is a UDF? Basic Syntax
A UDF is a DAX expression that takes parameters and returns a result, stored in the model under a name. It is defined using the new FUNCTION keyword and => the body to the right of the keyword consists of standard DAX. The general pattern is as follows:
DEFINE
/// Fonksiyonun açıklaması
/// @param {Tür} ParametreAdı - Parametre açıklaması
/// @returns Dönüş değerinin açıklaması
FUNCTION <FonksiyonAdı> =
( <parametre> [: <tür> <alt tür> <mod>] [= <varsayılan>], ... ) =>
<gövde>
Your first function: Adding VAT
Open the DAX query view and run the following query:
DEFINE
/// Tutara KDV ekler. Oran verilmezse %20 kullanılır.
/// @param {NUMERIC} tutar - KDV hariç tutar
/// @param {NUMERIC} [oran] - İsteğe bağlı KDV oranı, varsayılan 0.20
/// @returns KDV dahil tutar
FUNCTION KdvEkle =
( tutar : NUMERIC, oran : NUMERIC = 0.20 ) =>
tutar * ( 1 + oran )
EVALUATE
{ KdvEkle ( 100 ), KdvEkle ( 100, 0.10 ), KdvEkle ( 100, 0.01 ) }
// 120, 110, 101
As is, the function exists only for that query. To save it to the model, use the "Update model with changes" button at the top or the "Update model: Add new function" link that appears just above the function. The saved function appears under the "Functions" node in the Model Explorer.
You can now call it just like any built-in function in any measure:
Ciro KDV Dahil = KdvEkle ( [Ciro] )
Ciro KDV Dahil (Temel Gıda) = KdvEkle ( [Ciro], 0.01 )
When using it in a calculated column, make sure the return type is consistent; if necessary, CONVERT :
Satır Tutarı KDV Dahil = CONVERT ( KdvEkle ( 'Satış'[Tutar] ), CURRENCY )
/// Why are comments important?
Lines starting with three slashes (///) are the function’s documentation. When you type the function name, IntelliSense displays this description and the signature. // Or /* */ comments do not appear in IntelliSense. Comments INFO.USERDEFINEDFUNCTIONS() are also reflected in the output; in other words, well-written comments serve as a living catalog of your model. AI tools like Copilot can also use typed signatures and comments to directly find and call these functions, rather than having to infer your business logic.
Naming Conventions
- The function name must be unique within the model and must not conflict with a built-in DAX function or a reserved word (
MEASURE,FUNCTION,DEFINE). - Letters, numbers, and underscores are allowed; spaces and special characters are not allowed. It’s safest to avoid Turkish characters (ş, ğ, ı, ç, ö, ü).
- You can create a namespace using a period:
Satis.GecenYilaGore,Finans.BrutMarj. A name cannot start or end with a period, and two periods cannot appear consecutively. - Periods cannot be used in parameter names.
- SQLBI recommends using PascalCase in UDFs to distinguish them from built-in UPPERCASE functions.
Where are functions written and managed?
UDFs are first-class objects in the model; they can be accessed from many tools, such as tables and measures. Which one you choose depends on your work style.
| Tool | Best suited for | Note |
|---|---|---|
| DAX Query View (Desktop) | Writing, testing, quick experimentation | Right-click in the model explorer › Quick queries: Evaluate, Define and evaluate, Define all functions in this model |
| TMDL view (Desktop) | Code-focused batch editing | createOrReplace scripts; written to the model using the Apply button at the top. Right-click › Script TMDL to |
| Model view (Desktop) | Quickly create a single function | Functions › New Function, edit in the formula bar |
| Modeling on the Web (Service) | Editing without the desktop, team collaboration | Introduced with GA |
| Power BI Project (PBIP) + Git | Version control, code review | Functions definition/functions.tmdl are stored in a file |
| Tabular Editor 3 | Enhanced authoring experience, package management | Supported since 3.23.0; 3.26.2+ for optional parameter syntax |
| SSMS 22.5+ | Scripting, DBA workflows | No syntax highlighting or IntelliSense |
| XMLA endpoint / Semantic Link Labs | Automatic deployment to dozens of models | Scripting from Fabric notebooks |
The TMDL equivalent of the same VAT function looks like this:
createOrReplace
/// Tutara KDV ekler. Oran verilmezse %20 kullanılır.
function KdvEkle =
( tutar : NUMERIC, oran : NUMERIC = 0.20 ) =>
tutar * ( 1 + oran )
There are two DMV functions for querying functions in the model:
// Tam meta veri (yazma yetkisi gerektirir)
EVALUATE INFO.USERDEFINEDFUNCTIONS ()
// Yalnızca adlar ve sınırlı bilgi
EVALUATE INFO.FUNCTIONS ( "ORIGIN", "2" )
A small but important detail: DEFINE FUNCTION Functions that you haven’t written using “ and saved to the model INFO.USERDEFINEDFUNCTIONS() will not appear in the results. These are query-scoped and ideal for testing.
Parameters: The true power—and the true pitfall—of UDFs
A UDF can accept between 0 and 256 parameters. Each parameter can have up to three optional hints: type, subtype, and pass-by-value mode. The syntax is ad : [tür] [alt tür] [mod] . If you do not specify any hints, the parameter AnyVal val .
Types: Value or Reference?
| Type | What it accepts | Mode |
|---|---|---|
AnyVal | Scalar or array (default) | val (expr allowed) |
Scalar | A single value; can be narrowed to a subtype | val (expr allowed) |
Table | A table | val (expr optional) |
AnyRef | Any model object reference | expr only |
MeasureRef | A metric reference | expr only |
ColumnRef | A column reference | only expr |
TableRef | A table reference | only expr |
CalendarRef | A calendar reference | only expr |
Value types (AnyVal, Scalar, Table) perform implicit type conversion; reference types do not. Scalar Subtypes for: Variant, Int64, Decimal, Double, String, DateTime, Boolean and those that include their numeric variants Numeric. When you write a subtype, Scalar you don’t need to specify it.
Type hints perform silent conversions; knowing this prevents surprises:
DEFINE
FUNCTION TamSayiyaCevir = ( x : INT64 ) => x
EVALUATE
{ TamSayiyaCevir ( 3.4 ), TamSayiyaCevir ( 3.5 ), TamSayiyaCevir ( "5" ) }
// 3, 4, 5 → ondalık yuvarlanır, metin sayıya döner
VAL and EXPR: the most critical concepts
VAL (early evaluation) calculates the argument once in the caller’s context before passing it to the function and provides the function with the ready result. EXPR (lazy evaluation), on the other hand, passes the expression without evaluating it. The function evaluates it internally, in a different filter context if necessary, and multiple times.
The best way to see the difference is to write the same body in both modes:
DEFINE
/// Hazır tablo alır; içerideki ALL hiçbir şey değiştiremez
FUNCTION SatirSay_Val = ( t : TABLE VAL ) =>
COUNTROWS ( CALCULATETABLE ( t, ALL ( 'Tarih' ) ) )
/// İfadeyi alır; ALL dışarıdan gelen yıl filtresini kaldırır
FUNCTION SatirSay_Expr = ( t : TABLE EXPR ) =>
COUNTROWS ( CALCULATETABLE ( t, ALL ( 'Tarih' ) ) )
EVALUATE
{
CALCULATE ( SatirSay_Val ( 'Satış' ), 'Tarih'[Yıl] = 2025 ), // yalnızca 2025
CALCULATE ( SatirSay_Expr ( 'Satış' ), 'Tarih'[Yıl] = 2025 ) // tüm yıllar
}
In the first function, 'Satış' the table is already calculated with the 2025 filter. The ALL('Tarih') is now inactive. In the second function, the function executes the expression in its own context, and the year filter is removed. One more detail: The VAL parameter inherits both the row and filter contexts from the caller; the EXPR parameter, since it is evaluated within the function, inherits only the filter context.
Practical rule:
- If you’re going to change the context within the function
CALCULATE, use EXPR or MeasureRef if you plan to change the context through time intelligence or iteration. - Use VAL if a fixed ratio, threshold, or one-time value is sufficient.
- Always explicitly specify the mode. As recommended by SQLBI, name EXPR parameters so they are recognizable by their names:
metrikExpr,tutarExpr. - Wrap the EXPR parameter inside the function
CALCULATE ( ... ). This way, whether the caller sends a measure or a rawSUMXexpression, the row context is correctly converted to the filter context. In SQLBI’s example, when this is omitted, the function returns an empty table.
Optional parameters (introduced with GA)
When you = <ifade> , the parameter becomes optional:
FUNCTION Topla = ( x : NUMERIC = 1, y : NUMERIC = 2 ) => x + y
// Geçerli çağrılar: Topla (), Topla ( 5 ), Topla ( , 10 ), Topla ( 5, 10 )
There are four rules to keep in mind:
- Position is flexible. An optional parameter can appear before required ones; it is skipped if the argument is left empty (
F ( 1, , 3 )). However, the minimum number of arguments is determined by the required parameter on the far right. - Scope is at the definition site. The default expression sees the names at the function’s definition site, not where it is called. It cannot refer to another optional parameter.
- Context is determined by the mode. Which context the default expression inherits depends on whether the parameter is VAL or EXPR.
- Type checking is conditional. A type hint applies to the default expression only when the default is used, and to the argument when an argument is provided.
Runtime type checking and helper functions
You can check the actual type of a parameter within the body ISNUMERIC, ISINT64, ISDECIMAL, ISDOUBLE, ISSTRING, ISBOOLEAN, ISDATETIME . This allows the same function to handle both a currency key (36) and its code ("USD") can be accepted by the same function. When working with reference parameters, two new information functions are also useful: TABLEOF the table to which a column, measure, or calendar belongs, NAMEOF and the name of the object as text.
Four Real-World Examples from Retail
The following examples assume a typical supermarket chain model: 'Satış' fact table, 'Tarih' calendar table, and [Ciro], [Maliyet] such as key metrics. You can test all of them in a DAX query first and then save them to the model.
1. Gross margin and performance band: a function within a function
A UDF can call another UDF. The margin calculation remains in one place, and the classification is built on top of it:
DEFINE
/// Brüt kârı ciroya oranlar. Ciro sıfırsa BLANK döner.
FUNCTION Finans.BrutMarj =
( ciro : DECIMAL, maliyet : DECIMAL ) =>
DIVIDE ( ciro - maliyet, ciro )
/// Marjı banda ayırır: >= %40 Güçlü, >= %20 Orta, altı Zayıf.
FUNCTION Finans.MarjBandi =
( ciro : DECIMAL, maliyet : DECIMAL ) =>
VAR m = Finans.BrutMarj ( ciro, maliyet )
RETURN
SWITCH ( TRUE (),
ISBLANK ( m ), BLANK (),
m >= 0.40, "Güçlü",
m >= 0.20, "Orta",
"Zayıf" )
EVALUATE
{ Finans.MarjBandi ( 250000, 175000 ) } // "Orta" (%30)
Usage in measures:
Brüt Marj % = Finans.BrutMarj ( [Ciro], [Maliyet] )
Marj Bandı = Finans.MarjBandi ( [Ciro], [Maliyet] )
2. Year-over-year change: for any metric
In the classic method, you would write the same template separately for Revenue YoY, Receipt Count YoY, and Average Basket Size YoY. MEASUREREF But COLUMNREF with a single function:
DEFINE
/// Bir ölçünün geçen yılın aynı dönemine göre değişimi.
/// @param {MEASUREREF} olcu - Karşılaştırılacak ölçü
/// @param {COLUMNREF} tarih - Takvim tablosunun tarih sütunu
/// @param {BOOLEAN} [yuzde] - TRUE: yüzde, FALSE: fark. Varsayılan TRUE
FUNCTION Satis.GecenYilaGore =
(
olcu : MEASUREREF,
tarih : COLUMNREF,
yuzde : SCALAR BOOLEAN VAL = TRUE
) =>
VAR Bu = olcu
VAR Onceki = CALCULATE ( olcu, SAMEPERIODLASTYEAR ( tarih ) )
RETURN
IF ( yuzde, DIVIDE ( Bu - Onceki, Onceki ), Bu - Onceki )
Ciro YoY % = Satis.GecenYilaGore ( [Ciro], 'Tarih'[Tarih] )
Fiş Sayısı YoY % = Satis.GecenYilaGore ( [Fiş Sayısı], 'Tarih'[Tarih] )
Ciro YoY Fark (₺) = Satis.GecenYilaGore ( [Ciro], 'Tarih'[Tarih], FALSE )
The metric reference (MEASUREREF) is recalculated in every context where it is used; CALCULATE which is why the date filter inside it works. If the metric’s value, VAL, were passed as a parameter, Onceki , it would still equal this year’s value.
3. Moving average: with a window size parameter
Daily revenue fluctuates; a 7- or 28-day moving average is used to identify the trend. Let’s make the window size a parameter and leave the default at 7 days:
DEFINE
/// Ölçünün son N günlük basit hareketli ortalaması. Varsayılan 7 gün.
FUNCTION Satis.HareketliOrtalama =
(
olcu : MEASUREREF,
tarih : COLUMNREF,
gun : SCALAR INT64 VAL = 7
) =>
VAR Pencere =
DATESINPERIOD ( tarih, MAX ( tarih ), - gun, DAY )
RETURN
AVERAGEX ( Pencere, olcu )
Ciro 7G Ort = Satis.HareketliOrtalama ( [Ciro], 'Tarih'[Tarih] )
Ciro 28G Ort = Satis.HareketliOrtalama ( [Ciro], 'Tarih'[Tarih], 28 )
AVERAGEX It evaluates the measure on every date row; since the measure reference carries an implicit CALCULATE , so context switching occurs automatically.
4. Competitor price index: same rule for both measure and column
For teams tracking competitor prices, the price difference percentage is needed both in a product-based column and as a measure at the aggregate level. The UDF ensures the same rule applies in both places:
/// Bizim fiyatın rakibe göre yüzde farkı. Pozitif: biz daha pahalıyız.
FUNCTION Fiyat.RakipFarki =
( bizimFiyat : DOUBLE, rakipFiyat : DOUBLE ) =>
DIVIDE ( bizimFiyat - rakipFiyat, rakipFiyat )
// Hesaplanan sütun (ürün satırı bazında)
Rakip Fark % = Fiyat.RakipFarki ( 'Ürün Fiyat'[Bizim Fiyat], 'Ürün Fiyat'[Rakip Fiyat] )
// Ölçü (seçili ürünlerin ortalaması üzerinden)
Ort. Rakip Fark % =
Fiyat.RakipFarki (
AVERAGE ( 'Ürün Fiyat'[Bizim Fiyat] ),
AVERAGE ( 'Ürün Fiyat'[Rakip Fiyat] )
)
From a single model to an enterprise library
The real benefit of UDFs is that they ensure consistency across the entire semantic layer, not just a single report. To achieve this, functions must be managed like code.
Organization with namespaces
Microsoft recommends starting function names with a team or domain prefix: Finans.Marj, Tedarik.TeminSuresi, IK.Ayrilma. A schema that works well in retail:
| Prefix | Content | Example |
|---|---|---|
Finans. | Margin, profitability, VAT, exchange rate | Finans.BrutMarj |
Satis. | Time comparisons, shopping cart, receipt | Satis.GecenYilaGore |
Stok. | Turnover rate, days of inventory, scrap | Stok.StokGunu |
Fiyat. | Competitor index, discount depth | Fiyat.RakipFarki |
Fmt. | Text formatting tools | Fmt.KisaTutar |
Version control with Git and TMDL
Functions in Power BI Project (PBIP) format definition/functions.tmdl are stored as plain text in the file. When you commit this file to Git, every change becomes a commit, and every rule discussion becomes a pull request. Microsoft’s recommended model is to establish a shared function library model tied to Git. Other model developers retrieve approved functions from here; this aims for a single source and zero drift. For deployment across multiple models, scripts can be written using the XMLA endpoint or Semantic Link Labs in Fabric notebooks.
DAX Lib: The App Store for Functions
SQLBI announced a community library called DAX Lib in December 2025. At the time of the announcement, the site featured over 30 libraries covering topics such as SVG visuals, measure formatting, unit conversions, and DAX patterns, and the number continues to grow. There are two ways to use it:
- Power BI Desktop: Copy the library’s TMDL script, paste it into the model’s TMDL view, and click Apply.
- Tabular Editor 3: Use the built-in DAX Package Manager to install, update, or roll back packages without opening a browser. The Install button appears only for models with a compatibility level of 1702 or higher. Packages use dot-separated names; for example
TimeSeries.MovingAveragetheTimeSeries.MovingAverage.Simplefunction.
Support for on-premises (private) libraries is planned for Tabular Editor but is not yet available. Until then, the shared library model + Git is the most reliable approach.
Governance: A function is a KPI definition
When a rule is moved to a function, it no longer determines the behavior of a single metric, but rather that of all metrics that call it. An incorrect definition can also spread just as easily everywhere. Therefore, document the following for each function: name, inputs, a plain-language description of the business rule, owner, and last validation date. Before converting the rule into a function, ensure alignment—for example, with the finance team; the syntax is the easy part.
UDF or calculation group?
Short answer: A UDF is for developers; a calculation group is for end users. The two are not competitors; they solve different problems. To summarize SQLBI’s comparison:
| Criterion | UDF | Calculation Group |
|---|---|---|
| Parameter | Explicit, typed parameters | None; comes with the setting filter context |
| Who can see it | Not visible in the report; used by the developer | Visible in the slicer, field list, and filters |
| Usage | With a different argument for each dimension | Same transformation for all dimensions in a visual |
| How it works | Opens like a macro where it is called | Converts the selected dimensions at the time of the query |
| Additional load | Parameters are resolved before execution | Performs an additional filtering operation at query time |
Select a UDF: If the same logic is needed in multiple places with different arguments, if the logic is too complex to copy, or if the measures in the same visualization need to carry their own parameters.
Select a calculation group: If a transformation—such as currency conversion or period comparison—needs to be applied to all measures in a visual and the user is expected to select it using a slicer (e.g., Actual / Budget).
The balance is shifting in time intelligence
The Tabular Editor team’s 2026 assessment shows that UDF-based open measures have surpassed calculation groups in many scenarios for time intelligence:
- Calculation elements affect headers, tool tips, measure filters in the filter pane, and conditional formatting. A UDF affects only the measure that explicitly calls it.
- In matrix layout, measures can be freely grouped and sorted; measure-based conditional formatting does not require the “twin measure” workaround.
- On the performance front, the author notes that UDF measures perform at least as well as calculation groups in practice; he also references a demo where calculation groups disrupt DAX fusion optimization in simple filters.
The trade-off: A separate measure is required for each calculation (which can be generated with C# scripts), and field parameters are needed for replacement with the slicer. In scenarios involving role-playing dimensions and TREATAS scenarios involving dimension-based relationships, calculation groups are still better. The most powerful design is often a combination of the two: the logic resides in the UDF, and the calculation element calls that UDF.
Under the Hood: Performance and Debugging
UDFs are not subprograms running in a call stack. According to SQLBI’s DAX internal structure documentation, the function body expands inline at the point where it is called (inline expansion). This has three practical consequences:
- The function itself imposes no additional overhead. The engine ultimately executes the expanded DAX; a well-written UDF is expected to perform as well as its hand-written equivalent.
- A poorly written UDF is bad everywhere. A function is called dozens of times. A single inefficient iteration in the body spreads throughout the entire model. That’s why library functions are the code that needs to be optimized the most.
- EXPR parameters are recalculated on every invocation. If you use the same EXPR parameter three times in the body, the expression may be evaluated three times—once per line if it’s within an iteration. If it’s needed multiple times in the same context, store the result in a
VARorADDCOLUMNScalculate it once and use it as a column.
VAL parameters, on the other hand, are calculated once before entering the function. If you do not need to change the context, VAL is both more predictable and generally more efficient.
Debugging Tips
- Test it in a query first. Write the function
DEFINE FUNCTIONandEVALUATEand test it with known inputs; save it to the model only afterward. - Log intermediate values. As in Microsoft’s currency conversion example,
EVALUATEANDLOG, you canVARvalue. - Measure, don’t guess. Compare the versions with and without functions using the Performance Analyzer and the Server Timings view in DAX Studio.
- Take type warnings seriously. The semantic parser in Tabular Editor 3.26.2 now issues warnings for arguments that do not match the declared type.
Limitations and Pitfalls
Just because a feature is GA doesn’t mean it’s fully complete. Here’s what you need to know before deploying to production (according to Microsoft Learn documentation):
At the definition level
- No recursion or mutual recursion.
- No overloading; you cannot define a function with the same name but a different signature. Branching within a single function using type-checked functions serves as an alternative.
- You cannot specify an explicit return type.
- UDFs
enumcannot return a value; UDFs cannot be passed to built-in parameters that expect an enum.
At the model level
- UDFs cannot be hidden, placed in view folders, or used with translations.
- Not supported in models that contain no tables.
- Object-level security (OLS) does not apply to functions. A function that calls a protected measure is not automatically protected; for users without access, it behaves as if that measure did not exist. Do not reveal hidden objects in function names and descriptions.
- Column references without a table prefix (unqualified) are interpreted as measures; they may not be automatically corrected during renaming. Always
'Tablo'[Sütun]specify the table prefix.
At the tool and connection levels
- In live-linked reports, report measures can use UDFs in the source model, but IntelliSense is not available; in a composite model, model measures cannot reference UDFs in the source model.
- There is no IntelliSense for UDFs in the visual calculation formula bar; additionally, visual calculations can only pass fields from the visual to the function.
- IntelliSense is limited in the VS Code TMDL extension and is not available in SSMS. The editor may incorrectly display a red underline when passing columns as EXPR parameters.
Common Mistakes
| Error | Symptom | Solution |
|---|---|---|
| Leaving the VAL parameter, which changes the context, | CALCULATE The filter inside is ineffective; previous year = this year | EXPR or MEASUREREF Use |
the EXPR expression CALCULATE without wrapping it | Raw SUMX results in an empty/incorrect result | In the body CALCULATE ( expr ) |
| Select the integer subtype | Decimals are silently rounded | DECIMAL, DOUBLE or NUMERIC |
| Variable return type in the calculated column | Type error or unexpected data type | Result CONVERT with |
| Undefined function | No one knows what it does; the catalog is empty | Add a /// a description |
| Old Tabular Editor version | Fake error in default parameters | 3.26.2 or higher |
Best practices: checklist
- Description of each function
///description and@param/@returnslabels. - The name follows PascalCase and includes a namespace prefix (
Satis.,Finans.). - The type and mode (VAL / EXPR) of each parameter are explicitly specified.
- Parameters that modify the context are of type EXPR or reference type and are
CALCULATEin the body. - Results of EXPR used multiple times
VARare stored in ''. - All column references are table-prefixed.
- The function was tested with known inputs in a DAX query before being saved to the model.
- The business rule was defined in consultation with the relevant department (finance, sales); the owner and validation date are recorded.
functions.tmdlOn Git, changes undergo code review.- Objects protected by OLS do not appear in the function name or description.
Conclusion: Where to start?
UDFs bring DAX one step closer to being a true programming language rather than just a formula language: abstraction, reusability, types, documentation, and version control. This comes with a new responsibility: the function is no longer just a measure, but a KPI definition—and it needs to be managed.
I suggest three steps to get started:
- Take inventory. Find the three most duplicated formulas in your model. These are typically margin, year-over-year comparison, and a ratio calculation.
- Clarify the definition. Write a one-sentence job description for each one and get it approved by the relevant department.
- Convert and measure. Turn them into UDFs, update existing metrics to call the function, and compare the results with the old values.
In the next post, I’ll walk you through the steps to move these functions into a shared library model connected to Git. Which formula in your model will you convert into a function first?
Resources
- DAX user-defined functions — Microsoft Learn
- DAX User-Defined Functions (Generally Available) — Power BI Updates Blog
- Introducing user-defined functions in DAX — SQLBI
- UDFs vs. calculation groups — SQLBI DAX Internals
- User-Defined Functions — SQLBI DAX Internals
- Introducing DAX Lib — SQLBI
- Introducing the DAX Package Manager — Tabular Editor
- When DAX UDF measures outperform calculation groups for time intelligence — Tabular Editor
- Tabular Editor 3: June 2026 Release
- DAX User-Defined Functions — Tabular Editor Docs
- Power BI June 2026: DAX UDFs Are Generally Available — Enterprise DNA
STAY IN THE LOOP
New guides, sorted by topic in a single email.
Every Monday, the week's new posts arrive grouped by topic. No new posts, no email.
