JSONata Expressions Reference

SignStack uses JSONata as its expression and transformation language. JSONata is an open-source query and transformation language for JSON maintained independently of SignStack — it's the same language used in tools like IBM App Connect and Ballerina, and you can experiment with it standalone in the JSONata Exerciser. This page is a working reference for the subset you'll reach for when building templates and blueprints; the official JSONata documentation is always the source of truth for the language itself.

Because JSONata is a widely-adopted, well-documented language, the skills you build here transfer to any JSON-querying task. It's also very well understood by modern LLMs:

  • Inside Studio — the built-in AI assistant can generate JSONata expressions directly from plain-English descriptions, with full awareness of your template's inputs and your blueprint's entity shapes. This is usually the fastest path.
  • Outside Studio — tools like ChatGPT, Claude, or Copilot will generally produce correct JSONata on the first try from a plain-English description. You can paste generated expressions into the JSONata Exerciser to verify against sample data before dropping them into a resource.

Where Expressions Are Used

Location YAML Property Purpose
Template Fields value.expr Calculate field display values
Template Fields includeIf Control field visibility (returns boolean)
Template Data data.transform Map inputs to HTML render context
Template Roles output.transform Extract data after role completes signing
Blueprint Steps includeIf Control step inclusion (returns boolean)
Blueprint Steps onComplete.updates[].transform Transform entity data after step completion
Blueprint Steps onComplete.validate Validate data before updates run
Blueprint Envelopes/Docs includeIf Control envelope/document inclusion
Blueprint Documents iterator Generate multiple document instances
Blueprint Participants name.expr, email.expr Dynamic participant info from input data
Blueprint/Template Inputs required Dynamic requirement (boolean or expression)

Expression Context

The variables an expression can see depend on where the expression lives. There are three distinct contexts:

1. Template field / data expressions

Anywhere inside a template — data.transform, field value.expr, includeIf. Entity inputs sit at the root, plus JSONata built-ins and any custom functions referenced by the template.

Variable What it is
$.<inputKey> Entity inputs passed to the template
#x3C;alias>() Custom functions referenced by the template
$now(), $exists(), etc. JSONata built-ins
$.client_info.name              // Entity 'client_info' field
$.deal_info.value               // Numeric property
$formatCurrency($.amount)       // Custom function

2. Template role output.transform

A role's output.transform runs when the role finishes signing. It sees:

Variable What it is
$.<inputKey> Entity inputs passed to the template
$.fields.<fieldKey> Field values the signer entered, captured from the signed document
#x3C;alias>() Custom functions referenced by the template
$now(), $exists(), etc. JSONata built-ins

See Template Role Outputs for the full picture.

3. Blueprint step onComplete

In a step's onComplete.validate or onComplete.updates[].transform, the expression's root context is an object with three keys — input, docOutputs, childOutputs.

Variable What it is
input.<entityKey> Every blueprint input at its current value
docOutputs.<documentKey>.<roleKey> Payload from the role's output.transform (participant steps only)
childOutputs.<childStepKey>.<entityKey> Each child's final entity snapshots (parent group steps only)
#x3C;alias>() Custom functions referenced by the blueprint
$now(), $exists(), $count(), etc. JSONata built-ins

See Step onComplete — Expression Context for the full reference.


Basic Syntax

Accessing Data

// Direct property access
$.client_info.name; // "Jane"

// Nested properties
$.client_info.address.city; // Access nested object

// Array access
$.items[0]; // First item
$.items[-1]; // Last item
$.items[0].name; // Property of first item

Operators

Comparison

Operator Meaning Example
> Greater than $.deal_info.value > 50000
>= Greater than or equal $.deal_info.value >= 50000
< Less than $.deal_info.value < 50000
<= Less than or equal $.deal_info.value <= 50000
= Equal (single =, not ==) $.status = "active"
!= Not equal $.status != "archived"

Logical

Operator Meaning Example
and Both must be true $.tier = "gold" and $.amount > 1000
or Either can be true $.tier = "gold" or $.tier = "platinum"
not(...) Negation not($exists($.cancelledAt))

Arithmetic

$.deal_info.quantity * $.deal_info.unitPrice
$.deal_info.total - $.deal_info.discount
$.deal_info.value / 100

Ternary (Conditional)

// condition ? value_if_true : value_if_false
$.client_info.tier = 'Premium' ? 0.1 : 0.05;

// Nested ternary
$.deal_info.value > 100000 ? 'Large' : $.deal_info.value > 50000 ? 'Medium' : 'Small';

String Concatenation

// Use & for string concatenation
$.client_info.firstName & ' ' & $.client_info.lastName;

// Results in: "Jane Doe"

Built-in Functions

String Functions

// Case conversion
$uppercase($.client_info.name); // "JANE"
$lowercase($.client_info.email); // "jane@example.com"

// Trimming
$trim('  hello  '); // "hello"

// Substring
$substring($.client_info.name, 0, 1); // "J" (first character)

// Length
$length($.client_info.name); // 4

// Contains
$contains($.client_info.email, '@'); // true

// Replace
$replace($.phone, '-', ''); // Remove dashes

// Split
$split('a,b,c', ','); // ["a", "b", "c"]

// Join
$join(['a', 'b', 'c'], ', '); // "a, b, c"

// Pad
$pad($.invoiceNumber, 6, '0'); // "000123"

Number Functions

// Formatting (currency, decimals)
$formatNumber($.deal_info.value, '$#,##0.00'); // "$50,000.00"
$formatNumber($.rate, '0.00%'); // "5.25%"

// Rounding
$round(3.456, 2); // 3.46
$floor(3.9); // 3
$ceil(3.1); // 4

// Math
$abs(-5); // 5
$sqrt(16); // 4
$power(2, 3); // 8

// Aggregation
$sum($.items.price); // Sum of all prices
$average($.items.quantity); // Average quantity
$min($.items.price); // Minimum price
$max($.items.price); // Maximum price
$count($.items); // Number of items

Date Functions

// Current timestamp
$now(); // ISO timestamp string

// Format dates
$fromMillis($toMillis($.signedDate), '[M01]/[D01]/[Y0001]');

// Common patterns:
// [Y0001]-[M01]-[D01]     → 2024-01-15
// [M01]/[D01]/[Y0001]     → 01/15/2024
// [MNn] [D1], [Y0001]     → January 15, 2024

Array Functions

// Filter
$.items[status = "active"]           // Items where status is active
$.items[price > 100]                 // Items with price > 100

// Map/Transform
$.items.name                         // Array of all names
$.items.(name & " - " & $string(price))  // Transform each item

// Sort
$sort($.items, function($a, $b) { $a.price > $b.price })

// Reverse
$reverse($.items)

// Append (concatenate two arrays)
$append($.items, [newItem])

// Shuffle (random order)
$shuffle($.items)

// Zip (pairwise combine)
$zip(["a", "b", "c"], [1, 2, 3])     // [["a",1], ["b",2], ["c",3]]

// Reduce
$reduce($.items.price, function($acc, $val) { $acc + $val }, 0)

Object Functions

// Merge objects (crucial for onComplete transforms)
$merge([$.client_info, { "status": "verified" }])

// Get keys
$keys($.client_info)                  // ["name", "email", "tier"]

// Lookup
$lookup($.client_info, "name")        // "Jane"

// Spread (decompose object into key/value pairs)
$spread($.client_info)                // [{"name": "Jane"}, {"email": "..."}, ...]

// Iterate over key/value pairs
$each($.client_info, function($v, $k) { $k & "=" & $v })

// Pick/rename specific keys with object construction
$.client_info.{ "name": name, "contact": email }

Type Functions

// Check existence
$exists($.client_info.middleName); // false if undefined

// Type checking
$type($.deal_info.value); // "number"
$type($.client_info.name); // "string"
$type($.items); // "array"

// Conversion
$string($.deal_info.value); // "50000"
$number('123.45'); // 123.45
$boolean($.client_info.isActive); // true/false

Conditional Functions

// Assert (throws if condition false)
$assert($.deal_info.value > 0, 'Value must be positive');

// Error (throws custom error)
$error('Invalid configuration');

Common Patterns

Safe Property Access

Handle potentially missing data:

// Using $exists
$exists($.client_info.middleName) ? $.client_info.middleName : '';

// Using default with ternary
$.client_info.tier ? $.client_info.tier : 'Standard';

Conditional Display

For includeIf (must return boolean):

// Show only for premium clients with large deals
$.client_info.tier = "Premium" and $.deal_info.value > 50000

// Show only if field exists
$exists($.deal_info.specialTerms)

// Show based on array content
$count($.items) > 0

Entity Updates (onComplete Transforms)

A transform's return value replaces the target entity wholesale — it is not auto-merged. To preserve existing fields, fold them in yourself with $merge. Read the previous state via input.<entityKey> and read what was just signed via docOutputs.<doc>.<role>:

// Add/update properties on the client_info entity
$merge([
  input.client_info,
  {
    "name":       docOutputs.intake_form.client.full_name,
    "email":      docOutputs.intake_form.client.email,
    "updatedAt":  $now()
  }
])

// Conditional update from a signing decision
$merge([
  input.deal_info,
  {
    "status": docOutputs.approval.reviewer.approved
                ? "approved"
                : "rejected"
  }
])

Without the $merge, the entity would be reduced to just the fields you returned — every other field would be dropped.

Formatting Values

// Currency
$formatNumber($.deal_info.value, '$#,##0.00');

// Percentage
$formatNumber($.rate / 100, '0.00%');

// Phone number
$substring($.phone, 0, 3) & '-' & $substring($.phone, 3, 3) & '-' & $substring($.phone, 6);

// Full name
$trim($.firstName & ' ' & ($exists($.middleName) ? $.middleName & ' ' : '') & $.lastName);

Array Transformations

// Sum line items
$sum($.lineItems.(quantity * unitPrice))

// Filter and count
$count($.items[status = "pending"])

// Get names as comma-separated list
$join($.participants.name, ", ")

// Check if any item matches condition
$count($.items[priority = "high"]) > 0

Debugging Tips

  1. Start simple: Build expressions incrementally, testing each part.

  2. Use Scenarios: Test expressions with different data sets using SignStack Scenarios.

  3. Check for typos: Property names are case-sensitive ($.client_info vs $.ClientInfo).

  4. Handle nulls: Use $exists() to check before accessing nested properties.

  5. AI assistance: Describe your logic in plain language and ask an LLM to generate the JSONata.


Further Resources