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
)
| Parameter | Type | Description |
|---|---|---|
name | string | Function name used in expressions |
fn | (args: unknown[]) => unknown | Implementation 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:
| Expression | args 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
- Expressions — built-in functions reference
- Custom Renderer — build output format adapters
- Architecture — understand the expression evaluation stage