Unify Applications
Logo
Page Functions (Unify Applications)

Page Functions (Unify Applications)

Logo

9 mins READ

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.

variable-panel.png
variable-panel.png

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's setValue(), 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

  1. Open the Data panel from the left rail (or press E).

  2. Click + (Add Resource) beside the page's name — or beside App for an app function — and choose Function.

  3. Write the body, wrapping each reference in {{ }}, and end it with return.

  4. Under Run Behaviour, choose Automatic or Manual.

  5. 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, func_ + random suffix. Bindings read the function via this ID: {{ func_BLV9n['value'] }}.

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 return statement.

runBehaviour

Optional

One of "automatic" or "manual" (see below). Defaults to automatic when absent.

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 (undefined or 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: An async body returns a Promise, so the function's value is left unset.

  • No Promise return 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/setInterval callbacks 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:

ScopeWhere DefinedReadable FromUse For
Page functionA specific page's function listBindings and actions on the same page onlyPage-specific formatting, validation, calculations
App functionGlobal page's function listBindings and actions on any page in the appShared 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:

  1. Trigger Function — in Select Manual Function, pick validateContactForm.

  2. Control Variable — set var_formErrors to {{ func_validateContactForm['value'] }}. Show each message next to its field by binding to, for example, {{ var_formErrors['value']['email'] }}.

  3. 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.

FAQs

Can I use async/await or fetch inside a function?

No. Functions are synchronous only. If the body is async or returns a Promise, the function's value is left unset (undefined), so the result of a fetch never reaches the function. For HTTP calls, use a data source. For multi-step async sequences, use action chains with success and error callbacks.

What is the difference between automatic and manual run behaviour?

An automatic function re-runs whenever any value it references (variable, data source result, another function, page input) changes. A manual function runs only when a Trigger Function action fires — reading it in a binding doesn't run it, and its value stays unset until the first trigger. 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?

You can't call a function, but you can read another function's result in the body: {{ func_otherFunc['value'] }}. The reading function then depends on the other function and re-runs when its result changes.

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?

A function body can call a variable's setValue(), but it isn't recommended: functions are for computing values, and side effects belong in action chains. Use a Control Variable action instead — for example after a Trigger Function action, setting the variable to the function's value. In an automatic function, writing to a variable the function also reads can cause an endless re-run loop.