What Is a Page Function?
A page function is a named, JavaScript-bodied piece of logic attached to a page (or to the global page for app-scope functions). Unlike block events, which fire in response to a single user gesture, a function computes a value: an automatic function re-runs whenever the values it depends on change, and a manual function runs when a Trigger Function action fires. A function takes no arguments — it reads what it needs from the page's context.


Key characteristics:
Synchronous only. Promises and async/await are not supported. If the body returns a Promise, the function's value is left unset (
undefined).Results are available as bindings. A function is exposed as state, not as something you call. Read its result with
{{ func_abc['value'] }}and any error with{{ func_abc['error'] }}.Computation, not side effects. A body can read variables, data sources, block state, other functions, page inputs and
userContext. It can call a variable'ssetValue(), but writes, navigation and API calls belong in action chains — in an automatic function, writing to something the function reads can make it re-run in a loop.
Create a Function in the Builder
Open the Data panel from the left rail (or press E).
Click + (Add Resource) beside the page's name — or beside App for an app function — and choose Function.
Write the body, wrapping each reference in
{{ }}, and end it withreturn.Under Run Behaviour, choose Automatic or Manual.
Click Save & Run (or Save for a manual function) and check the result on the Output tab.
For the full walkthrough, see Page Functions. This page shows stored configuration, where entities are referenced by ID (var_abc, func_abc). In the builder, insert references from the editor's autocomplete instead of typing IDs.
Stored Configuration Shape
A function is stored as an entry in page.properties.pageFunctions. Here is the shape of a real entry:
JSON — a manually-run function
{ "id": "func_BLV9n", "name": "getFormattedPrice", "functionBody": "const price = {{ var_price['value'] }};\nconst currency = {{ var_currency['value'] }} || 'USD';\nreturn new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(price);", "runBehaviour": "manual", "createdTime": 1750196246281 }
JSON — an automatically-run function
{ "id": "func_totals", "name": "computeCartTotal", "functionBody": "const items = {{ var_cartItems['value'] }} || [];\nreturn items.reduce((sum, item) => sum + item.price * item.qty, 0);", "runBehaviour": "automatic", "createdTime": 1750196246281 }
Field Reference
Field | Required | Description |
|---|---|---|
id | Required | Stable handle, |
name | Required | Human label shown in the builder. Must be unique, ignoring case, across the page's and app's variables, functions, blocks, data sources and folders. Renaming does NOT change the ID. |
functionBody | Required | JavaScript source. The body of a function declaration — last executed expression is NOT automatically the return value; use an explicit |
runBehaviour | Optional | One of |
createdTime | Required | Epoch-ms number. Stamped at creation. Not read by the runtime. |
parentId | Optional | Optional folder ID for organizing functions in the builder's left panel. Organizational only. |
Run Behaviour
Automatic
An automatic function re-runs whenever any of the values it references (variables, data sources, other functions, page inputs, user context) change. The builder works out these dependencies from the function body when you save it, so a value referenced inside a branch that didn't run is still a dependency.
Good for: computed values that should always stay in sync — a formatted total, a filtered list, a derived label.
Caveat: the function runs at page load with the initial state of all its dependencies. If a dependency is loading asynchronously, the function may return an interim result (
undefinedor empty) until the dependency resolves.
Warning: Automatic functions run outside of row context. They have no currentRow or any other block-local binding that is only available inside a repeated block. If you need per-row logic, compute the value in the cell's own binding (see Row-scoped Bindings), or trigger a manual function from the row's event — the triggering action passes the row in, so the body can read it as {{ currentRow }}.
Manual
A manual function runs only when a Trigger Function action fires (or when func_abc.trigger() is called). The platform never runs it on its own — not at page load, not when state changes, and not when a binding reads it. Bindings read its last value, which stays unset (and isEvaluated stays false) until the first trigger.
Good for: transformations triggered by user actions, one-shot computations, logic that should run in response to an event rather than track data continuously.
Note: if a binding needs a value at page load, use an automatic function instead.
Synchronous-Only Execution
Functions execute synchronously. This means:
No
await: Anasyncbody returns a Promise, so the function's value is left unset.No
Promisereturn values: If you return a Promise (or a function or symbol), the function's value is left unset (undefined) — not the resolved value.No usable
fetch: Function bodies aren't blocked from browser APIs the way bindings are, but the body runs synchronously and a returned Promise is discarded, so a request's result never reaches the function's value. Fetch data with a data source.Timers:
setTimeout/setIntervalcallbacks fire outside the render cycle and do not automatically trigger a re-render. Avoid them.
Note: Async alternatives: If you need async logic (API calls, timers), use a data source for fetching, or use action chains (onSuccessActions, onErrorActions) for sequencing steps that depend on async results. Functions handle synchronous transformation only.
Writing the Function Body
The function body is a standard JavaScript function body. You have access to:
Variables —
{{ var_abc['value'] }}Data source results —
{{ ds_users['data'] }}Page inputs (path parameters and query string values) —
{{ pageInputs['key'] }}User context —
{{ userContext }}(the signed-in user's profile). Function bodies have no auth-check helper; use a Run Script action for that.Standard browser globals:
Math,Date,JSON,Intl,console,Object,Array, etc.
Wrap every reference in {{ }}: the braces are removed before the body runs, and they are how the builder works out the function's dependencies. There is no import or require, and no third-party libraries are available.
You must use an explicit return statement to produce a value — the function body is not an arrow function with an implicit return.
Calling Functions
From a Binding Expression
Read a function's state by its ID inside any {{ }} expression. Functions take no arguments and are never called with ():
Reading a function from a binding
{{ func_BLV9n['value'] }} // the last returned value {{ func_BLV9n['value']['total'] }} // a key, when the body returns an object {{ func_BLV9n['error'] }} // the error, when the body threw {{ func_BLV9n['isEvaluated'] }} // true once the function has run
From the Trigger Function Action
Use the Trigger Function action (actionType: "triggerFunction") to run a manual function as a step in an action chain. Its picker, Select Manual Function, lists manual functions only. If the function throws, the action's error branch runs.
Action payload — trigger a manual function
{ "actionType": "triggerFunction", "payload": { "functionId": "func_computeTotals" } }
Scope: Page vs App Functions
Like variables, functions have two scopes:
| Scope | Where Defined | Readable From | Use For |
| Page function | A specific page's function list | Bindings and actions on the same page only | Page-specific formatting, validation, calculations |
| App function | Global page's function list | Bindings and actions on any page in the app | Shared formatters, validators, reusable calculations |
Automatic Re-run Gotchas
Deferred and coalesced re-runs
When multiple reactive dependencies change in rapid succession (e.g., a burst of variable writes), re-runs are debounced: the function runs once, about 300 ms after the last change. You will not see intermediate states in the function's output.
Only dependencies the builder can see are tracked
Dependencies come from the references the builder can parse in the function body. Reference values inside {{ }} (for example {{ var_abc['value'] }}). Values read through computed keys, indirect lookups or globals are not tracked, so changing them does not re-run the function.
Functions that read other functions
A function that reads another function's value depends on it, and re-runs when that function's result changes. Re-runs use the same debounce of about 300 ms, and a run that produces the same value and error as before does not notify anything that depends on the function.
Practical Examples
Currency formatter (automatic)
// Function: formatCurrency (automatic) const amount = {{ var_invoiceAmount['value'] }}; const locale = {{ userContext?.preferences?.locale }} || 'en-US'; const currency = {{ var_selectedCurrency['value'] }} || 'USD'; if (amount == null) return '—'; return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount); // Use in a binding: {{ func_formatCurrency['value'] }}
Filtered list builder (automatic)
// Function: getFilteredUsers (automatic) const users = {{ ds_allUsers['data'] }} || []; const searchTerm = {{ var_search['value'] }}?.toLowerCase() || ''; const statusFilter = {{ var_statusFilter['value'] }}; return users.filter(u => { const matchesSearch = !searchTerm || u.name?.toLowerCase().includes(searchTerm) || u.email?.toLowerCase().includes(searchTerm); const matchesStatus = !statusFilter || u.status === statusFilter; return matchesSearch && matchesStatus; }); // Use in a data table's records binding: {{ func_getFilteredUsers['value'] }}
Form validation (manual, called from action)
// Function: validateContactForm (manual) const name = {{ var_formName['value'] }}; const email = {{ var_formEmail['value'] }}; const errors = {}; if (!name?.trim()) errors.name = 'Name is required'; if (!email?.trim()) errors.email = 'Email is required'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) errors.email = 'Invalid email'; return errors; // {} when the form is valid
Wire it to the Submit button's On Click event as an action chain:
Trigger Function — in Select Manual Function, pick
validateContactForm.Control Variable — set
var_formErrorsto{{ func_validateContactForm['value'] }}. Show each message next to its field by binding to, for example,{{ var_formErrors['value']['email'] }}.Continue with the save only when there are no errors: give the next action the Only run when condition
{{ Object.keys(func_validateContactForm['value']).length === 0 }}.
Date formatting per row (cell binding)
// Functions take no arguments, so format per-row values directly in the cell's binding: {{ currentRow['createdAt'] ? new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(currentRow['createdAt'])) : '—' }}
Troubleshooting
Function value is empty after using async code
The function body contains an async keyword or returns a Promise, so its value is left unset. Functions are synchronous — remove async/await and replace async logic with a data source or action chain.
Automatic function does not update when a variable changes
Check that the variable is referenced in the function body inside {{ }} (for example {{ var_abc['value'] }}) — values read through computed keys or globals are not tracked. Also check that the variable's write landed on the correct ID (not a stale ID), and that the new result actually differs from the old one.
Cannot access row inside an automatic function
Expected behavior — automatic functions run at the page level, not inside a repeated block, and functions take no arguments. To use row data, compute the value in the cell's own binding with {{ currentRow }}, or switch the function to manual and trigger it from the row's event with the Trigger Function action — the body can then read the row as {{ currentRow }}.
Function returns undefined at page load
This is usually a timing issue — the function's dependencies (data sources, async operations) haven't resolved yet. Return a safe default at the top of the function body: if (!{{ ds_xyz['data'] }}) return ''; and the value will update once the data source resolves and triggers a re-run.