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 runs whenever it is called — from an action, from a binding expression, or automatically on every re-render.
Key characteristics:
Synchronous only. Promises and async/await are not supported. Any returned Promise is silently discarded — the value the platform sees is always the synchronous return value of the function body.
Return values are available as bindings. Call
{{ func_abc() }}in any binding expression to read the result.Side effects inside the function body run synchronously. Mutations to page state from inside the function body are visible immediately after the function returns.
Stored Configuration Shape
A function is stored as an entry in page.properties.functions. 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 (required) | Stable handle, func_ + random suffix. Bindings call the function via this ID: {{ func_BLV9n() }}. |
| name | Required (required) | Human label shown in the builder. Must be unique within the page's function namespace. Renaming does NOT change the ID. |
| functionBody | Required (required) | JavaScript source. The body of a function declaration — last executed expression is NOT automatically the return value; use an explicit return statement. |
| runBehaviour | Required (required) | One of "automatic" or "manual" (see below). |
| createdTime | Required (required) | Epoch-ms number. Stamped at creation. Not read by the runtime. |
| parentId | Optional (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 reactive values it reads (variables, data sources, URL parameters, user context) change. The platform tracks which reactive nodes the function accessed during its last run and re-schedules when any of those nodes emit a new value.
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 access to row, item, or any block-local binding that is only available inside a repeated block. If you need per-row logic, use a manual function called from a binding inside the repeater, or write an inline expression.
Manual
A manual function runs only when explicitly invoked — via the Trigger function action or by calling it in a binding: {{ func_abc() }}. The platform does not automatically re-run it when state changes.
Good for: transformations triggered by user actions, one-shot computations, side-effect-generating functions (e.g., writing to a variable).
Note: when called from a binding expression, a manual function does still run on every render — "manual" means the platform does not subscribe to its dependencies for auto-re-run; the binding expression itself triggers the call on each render cycle.
Synchronous-Only Execution
Functions execute synchronously. This means:
No
await: Top-levelawaitexpressions resolve immediately to a pending Promise, not the awaited value.No
Promisereturn values: If you return a Promise from a function, the caller sees the Promise object — not its resolved value — and binding expressions that depend on the function will render[object Promise]or fail silently.No
fetchinside functions: Network calls require an event-driven data source; you cannot make HTTP requests inside a function body.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 — via their IDs:
var_abc['value']Data source results — via their IDs:
ds_users['data']URL parameters — via
pageInputs['key']User context — via
user(the current user object)Page inputs — path parameters and query string values
Standard browser globals:
Math,Date,JSON,Intl,console,Object,Array, etc.
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
Call a function by its ID with the () call syntax inside any {{ }} expression:
Calling a function from a binding
{{ func_BLV9n() }} // call with no arguments {{ func_BLV9n(42, "hello") }} // call with positional arguments
From the Trigger Function Action
Use the Trigger function action (actionType: "controlBlockMethod" with methodName: "triggerFunction" — or the dedicated function trigger, depending on builder version) to call a function as a step in an action chain:
Action payload — trigger a manual function
{ "actionType": "controlBlockMethod", "payload": { "blockId": "func_computeTotals", "methodName": "run" } }
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), the platform coalesces the re-runs into a single deferred execution at the end of the current render cycle. You will not see intermediate states in the function's output.
Dependency tracking requires the value to be read
An automatic function tracks only the dependencies it reads during its last run. If a variable is never accessed in the current execution (e.g., behind a conditional branch that did not execute), changes to that variable will not trigger a re-run until the function first accesses it. For stable subscriptions, avoid optional reads inside early-return branches.
Functions that call other functions
Calling another function from inside your function body is supported — but the called function's reactive dependencies are NOT merged into the caller's tracked set. Changes to the callee's dependencies trigger a re-run of the callee's own subscriptions, not the caller's.
Practical Examples
Currency formatter (automatic)
// Function: formatCurrency (automatic) const amount = var_invoiceAmount['value']; const locale = user?.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() }}
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() }}
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'; // Write errors back to a variable: // Use setPageVariable to update var_formErrors with errors return Object.keys(errors).length === 0; // true = valid
Date formatter with fallback (automatic)
// Function: formatDate (automatic) // Called with an argument from inside a repeated block: // {{ func_formatDate(row['createdAt']) }} const [dateStr] = arguments; if (!dateStr) return '—'; try { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }).format(new Date(dateStr)); } catch { return dateStr; }
Frequently Asked Questions
Can I use async/await or fetch inside a function?
No. Functions are synchronous only. Any async/await or returned Promise is silently discarded — the caller sees the unresolved Promise object. For HTTP calls, use a data source. For multi-step async sequences, use action chains with onSuccessActions / onErrorActions callbacks.
What is the difference between automatic and manual run behaviour?
An automatic function re-runs whenever any reactive dependency it reads (variable, data source result, URL param) changes. A manual function runs only when explicitly called — via the Trigger Function action or by calling it in a binding expression. Use automatic for computed values that should always stay current; use manual for transformations triggered by user interaction.
Can I call one function from inside another?
Yes. Use {{ func_otherFunc() }} or reference the ID directly in the body. However, the reactive dependencies of the called function are not merged into the caller's tracked set — only the caller's direct reads are tracked for auto re-run purposes.
My automatic function returns undefined at page load — why?
The function's dependencies (typically data sources) haven't finished loading yet. Guard with an early return: if (!ds_xyz['data']) return ''; The function will re-run once the data source resolves and show the real value.
Can I write to a variable from inside a function?
You can call var_abc.setValue(newValue) from inside a function body to write to a variable. Be careful with automatic functions — if you write to a variable the function also reads, you risk an infinite re-run loop. Use this pattern only in manual functions triggered by explicit user actions.
Troubleshooting
Function returns [object Promise] or empty
The function body contains an async keyword or returns a Promise. 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 actually read during the function's last execution. If the variable access is inside a conditional that did not execute, the dependency is not tracked. Also check that the variable's write landed on the correct ID (not a stale ID).
Cannot access row inside an automatic function
Expected behavior — automatic functions run at the page level, not inside a repeated block. To access row data, call the function from a binding inside the repeater: {{ func_format(row['field']) }} and use arguments[0] inside the body. Or switch the function to manual and call it from within the row's event.
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.