DAX User-Defined Functions: la referencia que guardas y reutilizas

csalcedodatabi DAX Power BI Referencia
Editor TMDL de Power BI Desktop mostrando la función PerformanceBand completa — usa ANYREF, CALCULATETABLE, TABLEOF y SWITCH para clasificar cualquier medida contra su propia distribución estadística. En el panel derecho se ven las 6 funciones personalizadas del modelo y las 11 medidas.

En resumen — Desde junio 2026, Power BI Desktop incluye funciones personalizadas en DAX con disponibilidad general. Esta página es la referencia que yo quiero tener a mano: sintaxis, tipos de parámetros, modos de evaluación, utilidades y bugs conocidos — todo verificado contra las fuentes primarias.

Nota

¿Tienes prisa? Salta directo a la tabla de tipos de parámetros o a la sección de bugs conocidos y workarounds. El resto lo lees cuando lo necesitas.


1. Sintaxis básica

Una función personalizada en DAX usa la palabra clave FUNCTION con una flecha => para separar la firma del cuerpo.

En DAX Query View (para probar)

DEFINE
  FUNCTION NombreFuncion = (
      param1 : TIPO,
      param2 : TIPO = ValorDefault
  ) =>
      <cuerpo de la función>

EVALUATE NombreFuncion( ... )

En TMDL View (para guardar al modelo)

createOrReplace
  function NombreFuncion = (
      param1 : TIPO,
      param2 : TIPO = ValorDefault
  ) =>
      <cuerpo>

Nota

Guardar al modelo desde DAX Query View: usa “Update model with changes” para guardar todas las funciones del query, o “Update model: Add new function” para guardar solo la función donde está el cursor.


2. Los 8 tipos de parámetros

Los tipos se dividen en dos familias con comportamientos distintos:

TipoFamiliaModo por defectoCasting implícitoDescripción
AnyValValueVALAbstracto: cubre Scalar o Table
ScalarValueVALSolo escalares. Combinable con subtype
TableValueVALSolo tablas (incluye expresiones de tabla)
AnyRefExpressionEXPRCualquier expresión — el más permisivo
ColumnRefExpressionEXPRSolo referencias a columnas del modelo
MeasureRefExpressionEXPRSolo referencias a medidas del modelo
TableRefExpressionEXPRSolo tablas del modelo (no expresiones como FILTER)
CalendarRefExpressionEXPRSolo referencias a calendarios

Regla clave: los tipos *Ref siempre son EXPR — no puedes especificar VAL para ellos. Los tipos Value son VAL por defecto.

Subtypes de Scalar

SubtypeAcepta
VariantCualquier escalar
Int64 / IntegerNúmero entero
Decimal / CurrencyDecimal de precisión fija
DoubleDecimal punto flotante
String / TextTexto
DateTimeFecha/hora
Boolean / LogicalTRUE/FALSE
NumericCualquier número (Int64, Decimal, Double)

3. VAL vs EXPR — el concepto más importante

Este es el punto donde más confusión se genera. La diferencia afecta directamente cómo se comporta tu función con CALCULATE.

VAL — evaluación inmediata (eager)

El argumento se evalúa antes de entrar a la función. Hereda el row context y el filter context del caller.

FUNCTION TotalFilas = ( t : TABLE VAL ) =>
  COUNTROWS( CALCULATETABLE( t, ALL( 'Date' ) ) )

-- El filtro de año YA está aplicado cuando entra la tabla
-- CALCULATETABLE con ALL('Date') no puede quitarlo
CALCULATE( TotalFilas( 'Sales' ), 'Date'[Year] = 2025 )
-- → cuenta solo las filas de 2025

EXPR — evaluación diferida (lazy)

El argumento se evalúa dentro de la función. Solo hereda el filter context — no el row context.

FUNCTION TotalFilas = ( t : TABLE EXPR ) =>
  COUNTROWS( CALCULATETABLE( t, ALL( 'Date' ) ) )

-- La tabla se evalúa dentro, CALCULATETABLE puede modificar el contexto
CALCULATE( TotalFilas( 'Sales' ), 'Date'[Year] = 2025 )
-- → cuenta TODAS las filas (ALL('Date') elimina el filtro externo)

La regla con CALCULATE

-- ❌ Incorrecto: parámetro VAL → CALCULATE no puede modificar el contexto del valor
FUNCTION VentasFiltradas = ( tipo ) =>
  CALCULATE( SUM( Ventas[Monto] ), Ventas[Tipo] = tipo )

-- ✅ Correcto: ANYREF implica EXPR → CALCULATE funciona
FUNCTION VentasFiltradas = ( tipo : ANYREF ) =>
  CALCULATE( SUM( Ventas[Monto] ), Ventas[Tipo] = tipo )

Advertencia

AnyRef con o sin EXPR: escribir ANYREF o ANYREF EXPR produce exactamente el mismo resultado. Todos los tipos *Ref son forzosamente EXPR — escribirlo explícito es redundante pero válido.


4. Funciones de utilidad para UDFs

TABLEOF()

Retorna la tabla completa asociada a una columna, medida o calendario. Imprescindible cuando recibes un ANYREF y necesitas iterar la tabla padre.

FUNCTION ContarFilas = ( col : ANYREF ) =>
  COUNTROWS( TABLEOF( col ) )

-- Llamada:
EVALUATE ContarFilas( 'Sales'[Amount] )  -- retorna el total de filas de Sales

NAMEOF()

Retorna el nombre de una tabla, columna, medida o calendario como texto. Útil para validaciones y mensajes de error descriptivos.

-- Sintaxis completa:
NAMEOF( <referencia>, <component>, <escaped> )

-- component: TABLE, COLUMN, MEASURE, CALENDAR, FULL (default), SELF, PARENT
-- escaped:   ESCAPED (default), UNESCAPED, MINIMALLYESCAPED

NAMEOF( 'Sales'[Amount] )               -- "'Sales'[Amount]"
NAMEOF( 'Sales'[Amount], TABLE )        -- "'Sales'"
NAMEOF( 'Sales'[Amount], COLUMN )       -- "[Amount]"
NAMEOF( TABLEOF( colParam ) )           -- nombre de la tabla del parámetro

Nota

Limitación: NAMEOF no acepta variables ni expresiones dinámicas como argumento — solo referencias directas a objetos del modelo.


5. Parámetros opcionales

Cualquier parámetro puede tener un valor por defecto. Cuando el caller lo omite, se usa el default.

FUNCTION AgregarImpuesto = (
  monto   : NUMERIC,
  tasa    : NUMERIC = 0.19   -- opcional, default 19%
) =>
  monto * ( 1 + tasa )

-- Formas de llamar:
AgregarImpuesto( 100 )          -- usa tasa = 0.19  → 119
AgregarImpuesto( 100, 0.05 )    -- usa tasa = 0.05  → 105
AgregarImpuesto( 100, )         -- argumento vacío  → 119

Reglas:

  • Pueden estar en cualquier posición (no solo al final)
  • El default no puede referenciar otros parámetros de la misma función
  • El default solo puede referenciar nombres visibles donde se define la UDF, no donde se llama

6. Type checking dentro del cuerpo

Cuando un parámetro es AnyVal o sin tipo, puedes verificar el tipo en tiempo de ejecución:

ISNUMERIC( x )   ISNUMBER( x )    -- numérico (cualquier tipo)
ISDOUBLE( x )                     -- double
ISINT64( x )     ISINTEGER( x )   -- entero
ISDECIMAL( x )   ISCURRENCY( x )  -- decimal
ISSTRING( x )    ISTEXT( x )      -- texto
ISBOOLEAN( x )   ISLOGICAL( x )   -- booleano
ISDATETIME( x )                   -- fecha/hora

Ejemplo — aceptar clave numérica o código texto:

FUNCTION ObtenerMoneda = ( moneda ) =>
  IF(
      ISINT64( moneda ),
      LOOKUPVALUE( 'Moneda'[Nombre], 'Moneda'[ClaveMoneda], moneda ),
      LOOKUPVALUE( 'Moneda'[Nombre], 'Moneda'[Codigo],      moneda )
  )

ObtenerMoneda( 36 )     -- busca por clave numérica
ObtenerMoneda( "USD" )  -- busca por código texto

7. Bugs conocidos y workarounds

Advertencia

Bug 1 — Tipos específicos no aceptados en todos los call sites

ColumnRef, MeasureRef, CalendarRef y TableRef pueden producir errores de parser en algunos puntos de llamada.

Workaround documentado por Microsoft: usa AnyRef en lugar de los tipos específicos si aparecen líneas rojas o errores de validación.

Advertencia

Bug 2 — Líneas rojas en IntelliSense (falsos positivos)

Ciertos escenarios avanzados muestran líneas rojas o errores de validación al pasar columnas como parámetros EXPR o al usar referencias sin cualificar.

Las líneas rojas pueden ser falsos positivos — el código puede ejecutar correctamente aunque el editor lo marque como error.

Limitaciones generales (GA junio 2026):

  • Solo se pueden crear UDFs desde Power BI Desktop — no desde el Service
  • No se pueden ocultar, organizar en carpetas ni añadir traducciones
  • No soportan recursión ni sobrecarga de funciones (overloading)
  • Sin IntelliSense para Live Connect / Composite Models
  • OLS no se transfiere automáticamente hacia o desde funciones

8. Inspeccionar las UDFs del modelo

-- Ver todas las UDFs guardadas en el modelo
EVALUATE INFO.USERDEFINEDFUNCTIONS()

-- Ver funciones con metadata de origen
EVALUATE INFO.FUNCTIONS("ORIGIN", "2")

9. Convenciones de nomenclatura

Sin estándar oficial de casing, estas son las convenciones más usadas por la comunidad:

Tipo de parámetroSufijo sugerido
ColumnRef_Column
MeasureRef_Measure
TableRef_Table
CalendarRef_Calendar
AnyRef_Expr

Namespacing con dot-notation para bibliotecas y funciones compartidas:

-- Patrón recomendado para equipos y librerías
Empresa.Finance.CrecimientoYoY
DAXLib.SVG.BarraHorizontal
MiOrg.Scorecard.Promedio4Qtrs

10. Ejemplo magistral: PerformanceBand — descarga el PBIP

Nota

Código listo para usar → El proyecto completo está en GitHub: powerbi-pbip-tools / examples / DAX-User-Defined-Functions. Clona el repo, abre el .pbip y haz Refresh — la data se carga automáticamente desde GitHub, sin configurar rutas locales.

Esta función condensa todo lo que hace especiales a las DAX UDFs: recibe cualquier medida y cualquier dimensión como parámetros (ANYREF), usa TABLEOF() para navegar al contexto correcto, limpia el filtro externo con CALCULATETABLE + ALL() y clasifica el valor actual contra la distribución estadística real del grupo — sin umbrales arbitrarios.

Nota

Patrón clave: CALCULATETABLE( ADDCOLUMNS( DISTINCT(groupCol), "@val", CALCULATE(metric) ), ALL( TABLEOF(groupCol) ) ) — primero limpia el contexto externo, luego itera. Sin este orden, todos los productos heredan el filtro activo y el cálculo estadístico se contamina.

FUNCTION PerformanceBand = (
  metric   : ANYREF,   -- cualquier medida del modelo
  groupCol : ANYREF    -- cualquier columna de dimensión
) =>
  VAR _AllRows = CALCULATETABLE(
      ADDCOLUMNS( DISTINCT( groupCol ), "@val", CALCULATE( metric ) ),
      ALL( TABLEOF( groupCol ) )   -- limpia filtros externos antes de iterar
  )
  VAR _Mean    = AVERAGEX( _AllRows, [@val] )
  VAR _StdDev  = STDEVX.P( _AllRows, [@val] )
  VAR _Current = metric
  RETURN
  SWITCH(
      TRUE(),
      ISBLANK( _Current ),             "—",
      _Current >= _Mean + _StdDev,     "★ Top",
      _Current >= _Mean,               "↑ Sobre promedio",
      _Current >= _Mean - _StdDev,     "↓ Bajo promedio",
                                       "⚠ Rezagado"
  )

Las medidas que la usan son de una línea:

-- Clasifica productos por rentabilidad vs. el grupo completo
Profit Band = PerformanceBand( [Total Profit], financials[Product] )

-- Misma lógica, otra métrica — reutilización total
Sales Band  = PerformanceBand( [Total Sales],  financials[Product] )

Resultado real con datos financials (6 productos, 2013-2014):

ProductoTotal ProfitProfit BandTotal SalesSales Band
Paseo4.8M★ Top33M★ Top
VTT3.0M↑ Sobre promedio20.5M↑ Sobre promedio
Amarilla2.8M↓ Bajo promedio17.7M↓ Bajo promedio
Velo2.3M↓ Bajo promedio18.2M↓ Bajo promedio
Montana2.1M↓ Bajo promedio15.4M↓ Bajo promedio
Carretera1.8M⚠ Rezagado13.8M↓ Bajo promedio

Lo que resuelve: el mismo corte estadístico que tomaría una docena de medidas hardcodeadas se generaliza en una función + dos medidas de una línea, aplicable a cualquier métrica y cualquier dimensión del modelo.


11. DAX Lib y recursos de la comunidad

DAX Lib — lanzado por Marco Russo (SQLBI) en diciembre 2025: repositorio comunitario de UDFs reutilizables distribuidas como scripts TMDL.

Cómo importar una función de DAX Lib:

  1. Copia el script TMDL desde daxlib.org
  2. Pégalo en la vista TMDL de tu modelo en Power BI Desktop
  3. Guarda y listo

Alternativa con Tabular Editor 3: usa el DAX Package Manager para importar funciones de DAX Lib de forma automatizada.

Otros repositorios y referencias:


Fuentes

Esta referencia está construida sobre las siguientes fuentes primarias, verificadas independientemente:

¿Te resultó útil este artículo?

Tu apoyo me permite seguir creando contenido de calidad.

¡Contáctame!