Unify Logo Footer.svg
Unify Applications
Logo
Expression Syntax

Expression Syntax

Logo

4 mins READ

Expression Syntax

Everything between {{ and }} is an expression. There are two kinds — path bindings and full expressions — and the difference matters for how results are typed and when failures occur. A value with no {{ }} in it is plain text and is never evaluated.

Note: The content inside {{ }} is evaluated as real JavaScript against a context object where every entity on the page is already in scope by its name. This means all JavaScript operators, methods, and syntax work — and it also means name mistakes fail silently rather than showing an error.

Path Bindings

A path binding is a bare reference — a name followed by dots or brackets, with no spaces or operators inside:

Valid path bindings

{{user.name}} {{items[0]}} {{data['key with spaces']}} {{orders_table.selectedRow.id}} {{orders_query.data.objects}}

Path bindings are the most common form. They preserve the referenced value's type — an array stays an array, an object stays an object.

Full Expressions

The moment the content has spaces, operators, or a function call, it is evaluated as a full JavaScript expression:

Full expression examples

// Arithmetic {{ order.qty * order.price }} // Ternary / conditional {{ status === 'open' ? 'Active' : 'Closed' }} // Template literal {{ `Order #${order.id} — ${order.status}` }} // Array method with arrow function {{ orders_query.data.objects.filter(o => o.total > 100).length }} // Optional chaining and nullish coalescing {{ order?.customer?.email ?? 'No email' }} // Utility helper {{ utils.formatDate(order.createdAt, 'dd MMM yyyy') }} // Object literal lookup {{ ({ open: 'green', closed: 'red' })[order.status] }}

Type Rules

A single binding preserves its type

When the entire value is one binding (possibly with surrounding whitespace), the result keeps the referenced type:

ExpressionReferenced typeResult type
{{ orders_query.data.objects }}ArrayArray (can be used as Table data)
{{ order.qty * order.price }}Number (computed)Number
{{ is_active.value }}BooleanBoolean (can drive checkbox default)
{{ user.name }} String (leading/trailing spaces ignored)String

Mixing text and bindings always produces a string

The segments are joined with JavaScript's + operator, which coerces everything to a string:

Mixed text + binding

// This is a string, even though user.age is a number: "Age: {{ user.age }} years" → "Age: 32 years" // An object in mixed context is stringified: "Order: {{ order }}" → "Order: [object Object]" // ↑ Bind the specific field instead: "Order: {{ order.id }}"

Two adjacent bindings — the addition trap

Two bindings with no text between them are joined with JavaScript's + operator. If both resolve to numbers, they add rather than concatenate:

Adjacent bindings

// a = 1, b = 2: {{a}}{{b}} → 3 (numeric addition — probably not what you wanted) {{a}} {{b}} → "1 2" (string join — space between makes it text)

Bracket Notation vs Dot Notation

Both are valid JavaScript property access and both work in bindings. The builder writes bracket notation for paths it generates; you can use either:

NotationExampleWhen to prefer it
Bracket{{ block['value'] }}Keys that aren't valid identifiers (contain dashes, spaces, or start with a number). Prefer bracket for generated bindings.
Dot{{ block.value }}Simple alphanumeric keys. Shorter to read.

What's Not Valid

You writeWhat happens
{{}} or {{ }}Renders empty — an empty expression is not valid JavaScript; failures are silent.
{{ {{name}} }}Renders empty — a binding nested inside a binding is not valid; braces-inside-braces is not valid JavaScript in an expression context.
Hello {{user.name (unclosed)Not a binding — the value must have a matching }}, so this stays literal text exactly as typed.
{{ // my comment }}Renders empty — line breaks inside {{ }} are removed before evaluation, so everything after // (including the expression) is commented out.
{{ variables.myVar }}Renders empty — there is no variables namespace. Reference the entity by its binding name: {{ my_var.value }}.

Tip: An object literal {{ ({ open: 'green', closed: 'red' })[status] }} is valid — the builder tracks brace depth, so inner { } pairs belonging to your JavaScript are kept inside one binding segment. Only a nested {{ }} binding (a binding-inside-a-binding) is not allowed.

Supported JavaScript Syntax

The full range of JavaScript expression syntax is available inside {{ }}:

FeatureExample
Optional chaining{{ order?.customer?.email }}
Nullish coalescing{{ order.total ?? 0 }}
Ternary{{ active ? 'On' : 'Off' }}
Template literals{{ `${order.id}: ${order.status}` }}
Array methods{{ items.filter(i => i.qty > 0).length }}
Object destructuring / spread{{ ({ ...order, label: order.id })[key] }}
Math and string methods{{ Math.round(total * 100) / 100 }}
Logical operators{{ isAdmin && hasPermission }}
Comparisons{{ order.status === 'open' }}
Utility helpers (utils.*){{ utils.formatDate(order.createdAt, 'dd MMM yyyy') }}

Silent Failures

An expression that throws an error or evaluates to undefined produces an empty value — not an error screen. The page continues rendering. Common causes:

  • Referencing a name that doesn't exist in the page context

  • Calling a method on undefined without optional chaining

  • A misspelled entity name

  • A data source that hasn't loaded yet (the path exists but the value is undefined)

Tip: If a binding shows nothing and you don't know why, temporarily bind the whole data source object — e.g. {{ orders_query }} — to a Text block to see its full current state, or inspect the data source's Output panel after running it.