Button Events, Client Scripts, Look Up Records, and Data Binding In UI Builder

Prathmeshda
Tera Guru

Button or component event
↓
Event mapping
↓
Client Script or declarative handler
↓
Client state / data-resource input
↓
Look Up Records result
↓
List, form, message, navigation, or custom event

The most common implementation mistake is to build each piece separately and expect the page to connect itself. A button can exist without doing anything. A Client Script can exist without ever being invoked. A Look Up Records data resource can return valid JSON while the component is bound to the wrong path. The solution is to design the event payload, data-resource inputs, handler, and output binding as one contract.

This article builds a reusable Incident lookup flow in ServiceNow UI Builder. A user enters or selects an Incident sys_id, clicks a button, a Client Script validates the value and updates client state, the data resource evaluates the new input, and the page renders the returned record. The same pattern can be adapted for Cases, Requests, Changes, Assets, or custom tables.


Release note: ServiceNow documentation uses both data resource and the older Data Broker terminology. It also uses different lifecycle labels across releases, such as Data Fetch Succeeded and Operation Succeeded. The concepts in this article are stable, but the exact editor labels, available handlers, component events, and API methods must be verified against the release and UI Builder version in your instance

Prathmeshda_0-1793860256971.png

 

 

The button should not contain server logic. The Client Script should not bypass platform security. The data resource should not be treated as a visual component. Each layer has one job:

•Button component: emits a user interaction event.

•Event mapping: connects that event to a handler.

•Client Script: validates the payload, updates state, and coordinates supported page behavior.

•Client state: stores temporary values such as a selected sys_id or encoded query.

•Look Up Records: retrieves a record from the configured table and exposes the result.

•Component binding: renders the result in a form, list, text component, or custom component.

•Lifecycle event mapping: handles loading, success, and failure states.

This separation makes the page easier to test. If the result is empty, inspect the resource input and preview before changing the component. If the button does nothing, inspect the event mapping before rewriting the script.

What a data resource ?

A UI Builder data resource fetches or transforms data for use by components. ServiceNow documents data resources for server data, GraphQL, REST, transforms, client state, composite resources, controllers, and operations. A page can use inherited resources supplied by the surrounding application or local resources added and configured on the page.

For the example, add a local Look Up Records resource or the equivalent record lookup resource available in your release. Configure it for the incident table and expose an input for the record identifier. The exact input label can differ. It may appear as sys_id, record, recordSysId, or another resource-specific property. Do not guess the name—read the resource's Configure panel and Preview JSON.

A useful conceptual configuration looks like this:
Resource ID: lookup_incident
Table: incident
Record identifier input: @STate.incidentSysId
Evaluation: according to the resource's supported evaluation mode

When the resource returns a record, a component might bind to a path similar to:

@Data.lookup_incident.result.number.displayValue
@Data.lookup_incident.result.short_description.displayValue
@Data.lookup_incident.result.priority.displayValue


The actual path depends on the resource definition and release. The official ServiceNow example for a lookup resource shows the same principle with a binding such as @Data.lookup_record_1.result.number.displayValue.

 

Client State is the link between the event and the resource

Define a Page-scoped Client State Parameter incidentSysId of type String and blank default value. Client State should be used for transient page parameters. It is not a substitute for database field, persistent session parameter, or security.

Set the Look Up Records input to bind to the state parameter:

@STate.incidentSysId

The page now has the following clean dependency path:

incidentSysId changes → lookup_incident input changes → resource evaluates → components change

The state parameter can be set by the event handler or by the Client Script. Setting it from the event handler is preferable in case of simple transformations. Use the Client Script if you need validation, condition, multiple state updates, logging, custom event, or some operation which cannot be done declaratively.

Clicking the Button to Client Script

To begin with, drag and drop a button onto the canvas and set the button label as Load incident. Navigate to the button’s Events tab and find Button clicked event. Click on Add handler or Add event mapping based on your release version. In the lookup window, search for the Client Script and choose the script where you will handle the click event.

The UI Builder Client Script generally gets an object including properties like api, event, helpers, and imports. The event property is the most useful tool to debug since it contains the event name and payload fired by the component.

The following code simply logs the event and changes the state:

function handler({ api, event }) {
console.log('Button event:', event.name);
console.log('Button payload:', event.payload);

const incidentSysId = event.payload?.incidentSysId;

if (!incidentSysId) {
api.setState('lookupMessage', 'Enter or select an Incident first.');
return;
}

api.setState('incidentSysId', incidentSysId);
api.setState('lookupMessage', 'Loading incident…');
}

The structure of event.payload differs depending on the component used. A Button won't be able to provide the required value unless a button property is configured, value is bound to the button or a different input component is used (List, List Menu, Select, Record Picker). In case the value belongs to another component, bind its selected value to the Client State and then access the state in the button script.

For instance, if a Record Picker sets the selected record value into incidentSysId, the button script will not have to rely on a hardcoded payload path like:

function handler({ api }) {
const incidentSysId = api.state.incidentSysId;

if (!incidentSysId) {
api.setState('lookupMessage', 'Select an Incident first.');
return;
}

api.setState('lookupMessage', 'Loading incident…');
}

How to invoke the Look Up Records data resource

There are two safe ways. Select one compatible with the type and version of your resource.

Pattern A: declarative re-evaluation from a bound input

This pattern is recommended for an immutable lookup whose resource re-evaluates on change of its bound input:

Plain Text


Button clicked
↓
Client Script calls api.setState('incidentSysId', value)
↓
Look Up Records input @STate.incidentSysId changes
↓
Look Up Records re-evaluates
↓
DATA_FETCH_SUCCEEDED or DATA_FETCH_FAILED
↓
Components render the result

 

This pattern ensures that the client-side script remains concise, and the data resource configuration controls fetching logic. Dependency becomes apparent in the UI Builder editor.

Pattern B: explicit refresh of supported resource instances

The UI Builder API reference describes how to use api.data.<data_resource_id>.refresh() for refresh of supported data-resource instances. This API reference mentions which resource types support this method and specifies that the resource's mutates_server_data property should be set to false. It also describes how to use execute() for supported mutating resource types, where mutates_server_data=true and inputs match the resource contract.

Supported Refresh Pattern is like the following:

function handler({ api }) {
const incidentSysId = api.state.incidentSysId;

if (!incidentSysId) {
api.setState('lookupMessage', 'Select an Incident first.');
return;
}

api.setState('lookupMessage', 'Refreshing incident…');

// This can be used only when the configured resource
// is of refresh() type in the target UI Builder release
// and the operation is non-mutative.
api.data.lookup_incident.refresh();
}

This example should not be blindly copied for every Look Up Records resource. Resource may be defined as server-side data look up resource that evaluates by default on any change of input values, whereas the refresh() method provided by the API may be available only for certain types and configurations of resources. In case if the refresh() method is not available in the code suggestion list in Now Code Editor for your resource, use the bound input pattern instead.



Prathmeshda_1-1793860985862.png

 

1.The user clicks Load incident.

2.The Button component emits Button clicked.

3.UI Builder resolves the event mapping.

4.The mapped Client Script receives { api, event }.

5.The script validates the selected value.

6.The script updates incidentSysId or another declared state parameter.

7.The bound lookup input changes and the resource evaluates, or the script invokes a supported refresh.

8.The server-side data access returns a record or an error.

9.The resource exposes the result through its data binding path.

10.The form, list, or text components re-render.

11.Success and failure lifecycle events can trigger a message, spinner, alert, or custom event.

Connecting lifecycle events

Map the resource lifecycle events separately from the button event. Depending on the release, the event names may appear as Data Fetch Initiated, Data Fetch Succeeded, and Data Fetch Failed, or as operation-oriented labels.

A useful page has three handlers:

Data Fetch Initiated → set loading state / show spinner
Data Fetch Succeeded → clear loading state / show result
Data Fetch Failed → clear loading state / show safe error

 

For example, map the success event to a page Client Script:

function handler({ api, event }) {
api.setState('isLoading', false);
api.setState('lookupMessage', 'Incident loaded.');

console.log('Lookup succeeded:', event.payload);
}


Map the failure event to a different script:

function handler({ api, event }) {
api.setState('isLoading', false);
api.setState(
'lookupMessage',
'The Incident could not be loaded. Check the value and your access.'
);

console.error('Lookup failed:', event.payload);
}

Do not show raw server errors, SQL-like queries, stack traces, or sensitive identifiers to end users. Log diagnostic information only where your operational policy permits it. ServiceNow's event mapping documentation describes component events, page events, data resource events, and declarative action events. It also distinguishes handled events from dispatched events. A component event can be mapped to a Client Script, a state update, navigation, a modal, or a data-resource operation.

 

Adding a custom event

A custom event is needed when the Client Script wants to notify another component of the page about the occurrence of a business-level event. For instance, after the successful lookup, the script could dispatch INCIDENT_LOADED and then another mapping could open a details panel.

First define the dispatched event in the page definition with the payload such as:

Event name: INCIDENT_LOADED
Payload field: sysId, String

 

After that dispatch it in the script:
function handler({ api }) {
const sysId = api.state.incidentSysId;

if (!sysId) {
return;
}

api.emit('INCIDENT_LOADED', {
sysId
});
}

 

The event must be defined before using api.emit(). Also, its name and payload fields must match the definition of the event. For page-level custom events, it is recommended to use the uppercase names of the past-tense verbs, like INCIDENT_LOADED, FILTER_APPLIED, RECORD_SELECTED etc.

Custom event should describe what happened rather than prescribe actions which should follow after. This will allow different handlers to react in their own way, for example, one of them may open the modal, and another one update breadcrumb.

Complete example: Lookup for incidents

Configuration of the page

Prathmeshda_0-1793862307673.png

 



Create an input or a record picker which will put selected sys_id into incidentSysId variable. Create a button called Load incident. Create a Stylized Text field bound to lookupMessage. Create a form or a text/card region to display the result of the lookup.

 

Client Script for Button

function handler({ api }) {
const sysId = api.state.incidentSysId;

if (!sysId) {
api.setState('lookupMessage', 'Select an Incident before loading.');
return;
}

api.setState('isLoading', true);
api.setState('lookupMessage', 'Loading incident…');

// If the target resource exposes refresh() and is configured as
// non-mutating, refresh the configured lookup instance.
if (api.data.lookup_incident?.refresh) {
api.data.lookup_incident.refresh();
}
}

 

This optional chaining guard is needed to prevent script error when the method is not exposed by the resource. In the real page, it is better to define the allowed trigger pattern at configuration stage and keep the code more explicit without any fallback that may cover the configuration mistake.

Success handler

function handler({ api }) {
api.setState('isLoading', false);
api.setState('lookupMessage', 'Incident loaded successfully.');

api.emit('INCIDENT_LOADED', {
sysId: api.state.incidentSysId
});
}

 

Failure handler
function handler({ api, event }) {
api.setState('isLoading', false);
api.setState(
'lookupMessage',
'No Incident was returned, or you do not have permissions to view it.'
);

console.error('Incident lookup failed', event.payload);
}

 

In case api.emit() is used, INCIDENT_LOADED should be declared as a dispatched page event at first. If the page is going to use only local handlers, it can be removed along with the success message.

 
If you found this article useful, please mark it as Helpful. It helps others find the content more easily 👍

 

0 REPLIES 0