Skip to content

A New Era in DAX: User-Defined Functions (UDFs)

DAX updated 25.10.2026 23 min read Atılay Berber

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.

TimelineWhat Happened
June 2026GA: optional parameters, modeling on the web, editing in Model view, enabled by default
June 2026Tabular Editor 3.26.2: GA syntax and type mismatch warnings
December 2025SQLBI announced the DAX Lib community function library
September 2025General 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.

ToolBest suited forNote
DAX Query View (Desktop)Writing, testing, quick experimentationRight-click in the model explorer › Quick queries: Evaluate, Define and evaluate, Define all functions in this model
TMDL view (Desktop)Code-focused batch editingcreateOrReplace scripts; written to the model using the Apply button at the top. Right-click › Script TMDL to
Model view (Desktop)Quickly create a single functionFunctions › New Function, edit in the formula bar
Modeling on the Web (Service)Editing without the desktop, team collaborationIntroduced with GA
Power BI Project (PBIP) + GitVersion control, code reviewFunctions definition/functions.tmdl are stored in a file
Tabular Editor 3Enhanced authoring experience, package managementSupported since 3.23.0; 3.26.2+ for optional parameter syntax
SSMS 22.5+Scripting, DBA workflowsNo syntax highlighting or IntelliSense
XMLA endpoint / Semantic Link LabsAutomatic deployment to dozens of modelsScripting 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?

TypeWhat it acceptsMode
AnyValScalar or array (default)val (expr allowed)
ScalarA single value; can be narrowed to a subtypeval (expr allowed)
TableA tableval (expr optional)
AnyRefAny model object referenceexpr only
MeasureRefA metric referenceexpr only
ColumnRefA column referenceonly expr
TableRefA table referenceonly expr
CalendarRefA calendar referenceonly 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 raw SUMX expression, 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:

  1. 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.
  2. 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.
  3. Context is determined by the mode. Which context the default expression inherits depends on whether the parameter is VAL or EXPR.
  4. 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:

PrefixContentExample
Finans.Margin, profitability, VAT, exchange rateFinans.BrutMarj
Satis.Time comparisons, shopping cart, receiptSatis.GecenYilaGore
Stok.Turnover rate, days of inventory, scrapStok.StokGunu
Fiyat.Competitor index, discount depthFiyat.RakipFarki
Fmt.Text formatting toolsFmt.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:

  1. Power BI Desktop: Copy the library’s TMDL script, paste it into the model’s TMDL view, and click Apply.
  2. 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.MovingAverage the TimeSeries.MovingAverage.Simple function.

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:

CriterionUDFCalculation Group
ParameterExplicit, typed parametersNone; comes with the setting filter context
Who can see itNot visible in the report; used by the developerVisible in the slicer, field list, and filters
UsageWith a different argument for each dimensionSame transformation for all dimensions in a visual
How it worksOpens like a macro where it is calledConverts the selected dimensions at the time of the query
Additional loadParameters are resolved before executionPerforms 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:

  1. 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.
  2. 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.
  3. 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 VARor ADDCOLUMNS calculate 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 FUNCTION and EVALUATE and test it with known inputs; save it to the model only afterward.
  • Log intermediate values. As in Microsoft’s currency conversion example, EVALUATEANDLOG , you can VARvalue.
  • 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 enum cannot 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

ErrorSymptomSolution
Leaving the VAL parameter, which changes the context,CALCULATE The filter inside is ineffective; previous year = this yearEXPR or MEASUREREF Use
the EXPR expression CALCULATE without wrapping itRaw SUMX results in an empty/incorrect resultIn the body CALCULATE ( expr )
Select the integer subtypeDecimals are silently roundedDECIMAL, DOUBLE or NUMERIC
Variable return type in the calculated columnType error or unexpected data typeResult CONVERT with
Undefined functionNo one knows what it does; the catalog is emptyAdd a /// a description
Old Tabular Editor versionFake error in default parameters3.26.2 or higher

Best practices: checklist

  • Description of each function /// description and @param / @returns labels.
  • 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 CALCULATE in 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.tmdl On 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:

  1. Take inventory. Find the three most duplicated formulas in your model. These are typically margin, year-over-year comparison, and a ratio calculation.
  2. Clarify the definition. Write a one-sentence job description for each one and get it approved by the relevant department.
  3. 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

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.

Nothing is sent until you click the link in the confirmation email. Unsubscribe anytime with one click.

Found this useful? Share it

Leave a comment

Your email address will not be published. Required fields are marked *