Advanced

Custom Functions

Extend the expression engine with custom functions using registerFunction.

Overview

The expression engine ships with 9 built-in functions (see Expressions), but you can register your own to handle domain-specific formatting, calculations, or transformations.

registerFunction API

Use registerFunction to add a new function to the expression engine:

import { registerFunction } from '@nextreport/engine'

registerFunction('avg', (args: unknown[]) => {
  const array = args[0] as number[]
  const sum = array.reduce((a, b) => a + b, 0)
  return sum / array.length
})

Once registered, use it in any expression:

{{avg(scores)}}

Function signature

registerFunction(
  name: string,
  fn: (args: unknown[]) => unknown
)
ParameterTypeDescription
namestringFunction name used in expressions
fn(args: unknown[]) => unknownImplementation receiving parsed arguments

Examples

Average function

registerFunction('avg', (args: unknown[]) => {
  const values = args[0] as number[]
  if (values.length === 0) return 0
  const sum = values.reduce((a, b) => a + b, 0)
  return Number((sum / values.length).toFixed(2))
})

Usage:

Average score: {{avg(scores)}}

Percentage function

registerFunction('percent', (args: unknown[]) => {
  const value = Number(args[0])
  const total = Number(args[1])
  if (total === 0) return '0%'
  return `${((value / total) * 100).toFixed(1)}%`
})

Usage:

{{percent(completedTasks, totalTasks)}}

Date difference (days)

registerFunction('daysBetween', (args: unknown[]) => {
  const start = new Date(args[0] as string)
  const end = new Date(args[1] as string)
  const diffMs = end.getTime() - start.getTime()
  return Math.floor(diffMs / (1000 * 60 * 60 * 24))
})

Usage:

Duration: {{daysBetween(startDate, endDate)}} days

Truncate text

registerFunction('truncate', (args: unknown[]) => {
  const text = String(args[0])
  const maxLen = Number(args[1]) || 50
  if (text.length <= maxLen) return text
  return text.slice(0, maxLen - 3) + '...'
})

Usage:

{{truncate(description, 40)}}

Conditional badge

registerFunction('statusBadge', (args: unknown[]) => {
  const status = String(args[0]).toLowerCase()
  const labels: Record<string, string> = {
    active: 'Active',
    inactive: 'Inactive',
    pending: 'Pending Review',
  }
  return labels[status] || status
})

Usage:

Status: {{statusBadge(account.status)}}

Working with the args array

The args parameter is an array of values parsed from the expression. The expression engine resolves variable references before passing them to your function:

Expressionargs received
{{avg(scores)}}[[85, 90, 78]]
{{percent(done, total)}}[42, 100]
{{truncate(name, 20)}}["John Smith", 20]
{{daysBetween(start, end)}}["2026-01-01", "2026-04-12"]

Type coercion

Arguments arrive as their JavaScript types. Use explicit coercion when needed:

registerFunction('myFunc', (args: unknown[]) => {
  const num = Number(args[0])       // Coerce to number
  const str = String(args[1])       // Coerce to string
  const date = new Date(args[2] as string) // Parse date
  // ...
})

Registration timing

Register functions before calling renderReport. Functions registered after rendering has started will not be available for that render pass.

import { registerFunction, renderReport } from '@nextreport/engine'

// Register first
registerFunction('avg', (args: unknown[]) => {
  const values = args[0] as number[]
  return values.reduce((a, b) => a + b, 0) / values.length
})

// Then render
const result = renderReport(schema, data)

Overriding built-in functions

You can override built-in functions by registering a function with the same name. This is useful for locale-specific formatting:

registerFunction('formatCurrency', (args: unknown[]) => {
  const value = Number(args[0])
  const currency = String(args[1])
  // Your custom formatting logic
  return new Intl.NumberFormat('tr-TR', {
    style: 'currency',
    currency,
  }).format(value)
})

Next steps