Custom Functions

A Custom Function is a reusable, versioned snippet of JSONata logic — a composable primitive for business logic. Define it once and call it from any expression in your templates or blueprints — valueExpression, displayCondition, data.transform, or onComplete mappers.

Functions let you encapsulate complex business logic (currency formatting, tax calculations, date math) in a single place. When the logic changes, update the function — not every expression that uses it. Like all SignStack primitives, functions are versioned and authored wherever you prefer — Studio, the CLI as YAML in your repo, or the API.

YAML Format

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: format_currency        # Required, snake_case
  version: 1.0.0              # Required, semver
  name: Format Currency       # Required
  description: Formats a number as USD currency string
  labels:
    category: formatting

spec:
  params:                                          # Optional: function parameters
    - name: amount                                 # Valid JS identifier
      type: number                                # string, number, boolean, object, or array
      # itemType: string                          # Required when type is array (string, number, boolean, or object)
      # schema: address_schema@1.0.0              # Required when type is object (or array of objects)

  returnType:                                      # Required
    type: string                                  # string, number, boolean, object, or array
    # itemType: string                            # Required when type is array
    # schema: address_schema@1.0.0               # Required when type is object (or array of objects)

  body: "'$' & $formatNumber(amount, '#,##0.00')" # Required: the JSONata expression

Spec Fields

Field Required Description
params No Input parameters. Each has name, type, an itemType (required for array), and optional schema (for object or array-of-object types).
returnType Yes What the function returns. Has type, an itemType (required for array), and optional schema.
body Yes JSONata expression implementing the function. Can call any JSONata built-in ($formatNumber, $round, $now, ...) or another declared SignStack function (see functions below). Max 10,000 chars. Use YAML | for multi-line.
functions No References to other custom functions called via $<alias>() in the body.

Parameter Types

Type Description
string Text values
number Numeric values
boolean True/false
object Complex object (optionally validated against a schema)
array List of values — set itemType to the element type

When type: array, an itemType (string, number, boolean, or object) is required. A schema may be attached when type: object, or when type: array with itemType: object.

Examples

Simple String Function

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: get_full_name
  version: 1.0.0
  name: Get Full Name

spec:
  params:
    - name: firstName
      type: string
    - name: lastName
      type: string

  returnType:
    type: string

  body: "firstName & ' ' & lastName"

Currency Formatter

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: format_currency
  version: 1.0.0
  name: Format Currency
  description: Formats a number as USD currency string

spec:
  params:
    - name: amount
      type: number

  returnType:
    type: string

  body: "'$' & $formatNumber(amount, '#,##0.00')"

Math with Rounding

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: calculate_tax
  version: 1.0.0
  name: Calculate Tax

spec:
  params:
    - name: amount
      type: number
    - name: taxRate
      type: number

  returnType:
    type: number

  body: '$round(amount * taxRate, 2)'

Object Parameter with Schema

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: format_address
  version: 1.0.0
  name: Format Address
  description: Formats an address object as a single line string

spec:
  params:
    - name: address
      type: object
      schema: address_schema@1.0.0

  returnType:
    type: string

  body: |
    $.address.street & ', ' &
    $.address.city & ', ' &
    $.address.state & ' ' &
    $.address.zipCode

Multi-Line with Block Expression

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: calculate_probation_end
  version: 1.0.0
  name: Calculate Probation End Date

spec:
  params:
    - name: startDate
      type: string
    - name: durationMonths
      type: number

  returnType:
    type: string

  body: |
    (
      $start := $toMillis(startDate);
      $msPerMonth := 30 * 24 * 60 * 60 * 1000;
      $endMs := $start + (durationMonths * $msPerMonth);
      $fromMillis($endMs, '[Y]-[M01]-[D01]')
    )

Composing Custom Functions

A function can call another SignStack function once it's declared in the functions block:

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: format_total_compensation
  version: 1.0.0
  name: Format Total Compensation

spec:
  params:
    - name: base
      type: number
    - name: bonus
      type: number

  returnType:
    type: string

  functions:
    money: format_currency@1.0.0   # Local alias → versioned function reference

  body: '$money(base + bonus)'

$money(...) resolves to format_currency@1.0.0 at runtime — same call syntax as JSONata built-ins, just declared first.

Function Calling Another Function

apiVersion: signstack/v1beta2
kind: JsonataFunction

metadata:
  key: create_audit_entry
  version: 1.0.0
  name: Create Audit Entry

spec:
  params:
    - name: action
      type: string
    - name: userId
      type: string

  returnType:
    type: object
    schema: audit_entry@1.0.0

  functions:
    get_full_name: get_full_name@1.0.0

  body: |
    {
      "action": action,
      "userId": userId,
      "timestamp": $now(),
      "id": $string($random())
    }

Using Functions in Expressions

Register functions in a template or blueprint's functions map, then call them via $<alias>():

# In a template or blueprint
spec:
  functions:
    format_currency: format_currency@1.0.0
    get_full_name: get_full_name@1.0.0

Then in any JSONata expression:

# In a template field value
value:
  expression: '$format_currency($.position.salary)'

# In a data transform
data:
  transform: |
    {
      "fullName": $get_full_name($.employee.firstName, $.employee.lastName),
      "salary": $format_currency($.position.salary)
    }

# In a blueprint onComplete mapper
onComplete:
  updates:
    - key: employee
      transform: |
        $merge([
          $.inputs.employee,
          {
            "probationEnd": $calculate_probation_end($.inputs.position.startDate, 3)
          }
        ])

Versioning

Functions use semantic versioning. A function moves from Draft to a concrete published version (1.0.0); to make further changes, you cut a new version (1.1.0, 2.0.0). Published versions are immutable — every template or blueprint pinning format_currency@1.0.0 keeps invoking the exact logic it was designed against, even after 2.0.0 ships.

Draft → 1.0.0 (published, immutable)
          ↓ edit + publish
        1.1.0 (published, immutable)
          ↓ edit + publish
        2.0.0 (published, immutable)

Pick the version bump based on the change:

  • MAJOR (2.0.0) — Breaking changes (signature change, return type change, behavioral semantics flipped)
  • MINOR (1.1.0) — Backwards-compatible additions (new optional behavior)
  • PATCH (1.0.1) — Bug fixes (rounding correction, off-by-one)

Best Practices

  1. One concern per function — Keep functions focused. format_currency does one thing well.
  2. Use descriptive parameter namesamount and taxRate are clearer than a and r.
  3. Document with descriptions — Helps your team and AI tools generate correct usage.