Data Binding Overview
Data binding is how you make any property in the builder dynamic. Instead of typing a fixed value, you write an expression inside double curly braces — {{ }} — and reference live data: another block's state, a data source result, a page variable, the signed-in user, the current URL. The moment a value contains {{ }}, the builder treats it as an expression rather than plain text.
The Three Rules
| # | Rule | What it means |
| 1 | Write an expression anywhere a property accepts one | A block label, a visibility condition, a data source input, an action parameter — if you can type into it, you can usually bind it. |
| 2 | The expression sees the whole page | Every binding evaluates against a shared picture of your app: blocks by name, data sources, page variables, page functions, user context, theme, URL parameters, and built-in utilities. |
| 3 | It recomputes when its inputs change | When a referenced value changes — a user types, a data source finishes, a variable is set — every expression that reads it recomputes and the UI updates. You never manually refresh a binding. |
Basic Syntax
Wrap any expression in double curly braces. Everything between {{ and }} is evaluated as JavaScript against the page's data context:
Basic binding examples
// Simple field reference Hello {{ user_name_input.value }}! // Data source result {{ orders_query.data.objects }} // Computed value {{ orders_query.data.objects.length }} orders found // Conditional {{ order.status === 'open' ? 'Active' : 'Closed' }} // Loading state {{ orders_query.isLoading ? 'Loading...' : '' }}
How It Works
Detect {{ }}: Any string containing a complete
{{ }}pair is classified as dynamic. Plain text without the braces is returned as-is.Resolve names against the page context: Every entity on the page — blocks, data sources, variables, functions — is in scope by its name. Fixed namespaces like
userContext,theme, andutilsare always available. See Binding Context for the full list.Evaluate the expression: The content between the braces is evaluated as real JavaScript. A bare path reference (
{{user.name}}) returns the value; an expression ({{ qty * price }}) computes it.Render the result: A lone binding hands over the referenced value as-is (an array stays an array). Text mixed with bindings produces a string. An expression that fails produces an empty value — not an error screen.
Where Bindings Work
Bindings are accepted anywhere the platform evaluates config against the page context:
Block properties — labels, placeholder text, default values, visibility conditions, disabled states, styles
Data source inputs — the inputs to an API call or query, including filter values and paging parameters
Action parameters — the values passed to actions (navigate URL, notification message, variable value)
Page function bodies — the JavaScript code of a page function
Variable initial values — the starting value of a page variable
Condition expressions — visibility conditions, disable conditions, run conditions
Type Preservation
The type of a binding's result depends on what surrounds it:
| What you write | Result type | Example |
| A single binding alone | Keeps the referenced type — array stays array, object stays object, number stays number | {{ tags_array }} → ["a", "b"] |
| Text mixed with a binding | Always produces a string | Hello {{ name }}! → "Hello Priya!" |
| Two adjacent bindings with no text | JavaScript addition — numbers add, other types concatenate | {{ a }}{{ b }} where a=1, b=2 → 3 (not "12") |
Reactive Updates
Bindings recompute automatically when their inputs change — no polling or manual refresh. This is how a filter box narrows a table without event wiring: the table's data property is bound to a filtered expression, and as the user types, the expression recomputes and the table re-renders.
Example — Search-as-you-type without event handlers
// Table's data property: {{ orders_query.data.objects.filter(o => o.customerName.toLowerCase().includes( search_input.value?.toLowerCase() ?? '' ) ) }} // No event handler needed — as the user types in search_input, // the filter expression recomputes and the table updates.
The State Explorer
The State Explorer panel in the builder — "State available for this input. Click to map." — shows all values available for the current binding field. Clicking a value inserts its binding expression automatically, so you don't need to know entity IDs or exact paths by memory.
Failures Are Silent
An expression that fails — a reference to a non-existent name, a thrown error, a type mismatch — produces an empty value rather than an error screen. The page keeps rendering. This "forgiving" behavior is useful for partial states (a data source still loading), but it means bugs in bindings show up as missing content, not error messages. Always test bindings with the actual data shape from the Output panel.
Warning: Referencing an entity by display name instead of its correct binding path — e.g. {{ variables.myVar }} instead of {{ my_var.value }} — evaluates to undefined with no error. Always verify binding paths using the State Explorer or Output panel.
This Section
| Page | What it covers |
| Expression Syntax | Path bindings vs full expressions, type rules, what's invalid. |
| Binding Context | The full namespace — blocks, data sources, variables, fixed namespaces like userContext, theme, utils. |
| Built-in Utilities | The utils helpers for formatting dates, numbers, JSON, and downloading files. |
| Row-scoped Bindings | How currentRow and instances work inside Repeatables and Tables. |
| Filters, Sorting & Pagination | Wiring the Filters block to data sources; condition builders and sort controls. |
Common Binding Patterns
Getting the logged-in user's email
{{ userContext.email }} {{ userContext.id }} {{ userContext.name }}
Binding to a data source result
// All records (multiple-record source) {{ orders_query.data.objects }} // Count {{ (orders_query.data.objects || []).length }} // First record's field {{ orders_query.data.objects?.[0]?.customerName }} // Loading and error state {{ orders_query.isLoading }} {{ orders_query.error?.message }}
Reading a page variable
// A page variable named "selected_tab" exposes { value, setValue } {{ selected_tab.value }} // Setting it from a binding/script {{ selected_tab.setValue('billing') }}
URL and navigation values
// Named URL path segment (/orders/:orderId) {{ pageParams.orderId }} // Values passed from another page {{ pageInputs.customerId }} // Current URL path {{ location.pathname }}
Frequently Asked Questions
Why does my binding show nothing when I'm sure the data source has data?
There are three common causes: (1) The data source is set to Manual run behavior and hasn't been triggered yet — it returns empty values until first run. (2) The binding path is wrong — the field name in your expression doesn't match the actual field name in the response. Run the data source and inspect its Output panel to confirm the exact path. (3) The top-level type is a mismatch — a single-record source returns an object, not an array; binding the object where an array is expected renders nothing.
Do I need to refresh bindings when data changes?
No — bindings recompute automatically when any referenced value changes. When a data source finishes loading, a variable is set, or a user types into an input, every expression that reads those values updates without any manual refresh action. The only exception is server-side data changes that don't change any bound input — for those, use polling or an explicit Trigger Data Source action.
Can I use bindings inside action parameters, not just block properties?
Yes — the same {{ }} syntax works in action parameters: navigation URLs, notification messages, API request bodies, and variable set values all accept binding expressions. This is how you pass the current user's ID to an API call, or build a dynamic navigation URL from page state.
What's the difference between data and rawData on a data source?
data is the result after any response transform has run — the shape your blocks should bind to. rawData is the untransformed HTTP response, exactly as the API returned it. When no transform is configured, the two are identical. Use rawData for debugging or when you need the original payload alongside a transformed result.