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
-
Start simple: Build expressions incrementally, testing each part.
-
Use Scenarios: Test expressions with different data sets using SignStack Scenarios.
-
Check for typos: Property names are case-sensitive (
$.client_infovs$.ClientInfo). -
Handle nulls: Use
$exists()to check before accessing nested properties. -
AI assistance: Describe your logic in plain language and ask an LLM to generate the JSONata.
Further Resources
- Official JSONata Documentation
- JSONata Exerciser - Test expressions online
- Templates — Dynamic Fields with Expressions - Template expression patterns
- Step
onComplete— Expression Context - onComplete transforms reference
