ui-builder-patterns
$
npx mdskill add serac-labs/serac/ui-builder-patternsBuild ServiceNow UI Builder pages with macroponents and data brokers
- Solve UI design and configuration tasks for Next Experience workspaces
- Uses ServiceNow APIs, GraphQL, and client-side scripting
- Analyzes page structure and component dependencies to suggest configurations
- Delivers ready-to-use macroponents and event handler code for developers
SKILL.md
.github/skills/ui-builder-patternsView on GitHub ↗
---
name: ui-builder-patterns
description: Build ServiceNow UI Builder / Next Experience pages — macroponents, GraphQL/script data brokers, client state parameters, event handlers (NOW_RECORD_LIST etc.), and now-record-list/form component configuration.
license: Apache-2.0
compatibility: Designed for Snow-Code and ServiceNow development
metadata:
author: serac
version: "1.0.0"
category: servicenow
tools:
- snow_workspace_create
- snow_query_table
- snow_find_artifact
- snow_execute_script_with_output
---
# UI Builder Patterns for ServiceNow
UI Builder (UIB) is ServiceNow's modern framework for building Next Experience workspaces and applications.
## UI Builder Architecture
### Component Hierarchy
```
UX Application
└── App Shell
└── Chrome (Header, Navigation)
└── Pages
└── Variants
└── Macroponents
└── Components
└── Elements
```
### Key Concepts
| Concept | Description |
| ---------------- | -------------------------------------------- |
| **Macroponent** | Reusable container with components and logic |
| **Component** | UI building block (list, form, button) |
| **Data Broker** | Fetches and manages data for components |
| **Client State** | Page-level state management |
| **Event** | Communication between components |
## Page Structure
### Page Anatomy
```
Page: incident_list
├── Variants
│ ├── Default (desktop)
│ └── Mobile
├── Data Brokers
│ ├── incident_data (GraphQL)
│ └── user_preferences (Script)
├── Client States
│ ├── selectedRecord
│ └── filterActive
├── Events
│ ├── RECORD_SELECTED
│ └── FILTER_APPLIED
└── Layout
├── Header (macroponent)
├── Sidebar (macroponent)
└── Content (macroponent)
```
## Data Brokers
### Types of Data Brokers
| Type | Use Case | Example |
| ------------- | ----------------- | ------------------ |
| **GraphQL** | Table queries | Incident list |
| **Script** | Complex logic | Calculated metrics |
| **REST** | External APIs | Weather data |
| **Transform** | Data manipulation | Format dates |
### GraphQL Data Broker
```javascript
// Data Broker: incident_list
// Type: GraphQL
// Query
query ($limit: Int, $query: String) {
GlideRecord_Query {
incident(
queryConditions: $query
limit: $limit
) {
number { value displayValue }
short_description { value }
priority { value displayValue }
state { value displayValue }
assigned_to { value displayValue }
sys_id { value }
}
}
}
// Variables (from client state or props)
{
"limit": 50,
"query": "active=true"
}
```
### Script Data Broker (ES5)
```javascript
// Data Broker: incident_metrics
// Type: Script
;(function execute(inputs, outputs) {
var result = {
total: 0,
byPriority: {},
avgAge: 0,
}
var gr = new GlideRecord("incident")
gr.addQuery("active", true)
gr.query()
var totalAge = 0
while (gr.next()) {
result.total++
// Count by priority
var priority = gr.getValue("priority")
if (!result.byPriority[priority]) {
result.byPriority[priority] = 0
}
result.byPriority[priority]++
// Calculate age
var opened = new GlideDateTime(gr.getValue("opened_at"))
var now = new GlideDateTime()
var age = gs.dateDiff(opened, now, true)
totalAge += parseInt(age)
}
if (result.total > 0) {
result.avgAge = Math.round(totalAge / result.total / 3600) // hours
}
outputs.metrics = result
})(inputs, outputs)
```
## Client State Parameters
### Defining Client State
```json
// Page Client State Parameters
{
"selectedIncident": {
"type": "string",
"default": ""
},
"filterQuery": {
"type": "string",
"default": "active=true"
},
"viewMode": {
"type": "string",
"default": "list",
"enum": ["list", "card", "split"]
},
"selectedRecords": {
"type": "array",
"items": { "type": "string" },
"default": []
}
}
```
### Using Client State in Components
```javascript
// In component configuration
{
"query": "@state.filterQuery",
"selectedItem": "@state.selectedIncident"
}
// Updating client state via event
{
"eventName": "NOW_RECORD_LIST#RECORD_SELECTED",
"handlers": [
{
"action": "UPDATE_CLIENT_STATE",
"payload": {
"selectedIncident": "@payload.sys_id"
}
}
]
}
```
## Events and Handlers
### Event Types
| Event | Trigger | Payload |
| --------------------------------- | --------------- | ----------------- |
| `NOW_RECORD_LIST#RECORD_SELECTED` | Row click | { sys_id, table } |
| `NOW_BUTTON#CLICKED` | Button click | { label } |
| `NOW_DROPDOWN#SELECTED` | Dropdown change | { value } |
| `CUSTOM#EVENT_NAME` | Custom event | Custom payload |
### Event Handler Configuration
```json
// Event: Record Selected
{
"eventName": "NOW_RECORD_LIST#RECORD_SELECTED",
"handlers": [
{
"action": "UPDATE_CLIENT_STATE",
"payload": {
"selectedIncident": "@payload.sys_id"
}
},
{
"action": "REFRESH_DATA_BROKER",
"payload": {
"dataBrokerId": "incident_details"
}
},
{
"action": "DISPATCH_EVENT",
"payload": {
"eventName": "INCIDENT_SELECTED",
"payload": "@payload"
}
}
]
}
```
### Client Script Event Handler (ES5)
```javascript
// Client Script for custom event handling
;(function (coeffects) {
var dispatch = coeffects.dispatch
var state = coeffects.state
var payload = coeffects.action.payload
// Custom logic
var selectedId = payload.sys_id
// Update multiple states
dispatch("UPDATE_CLIENT_STATE", {
selectedIncident: selectedId,
detailsVisible: true,
})
// Conditional dispatch
if (payload.priority === "1") {
dispatch("DISPATCH_EVENT", {
eventName: "CRITICAL_INCIDENT_SELECTED",
payload: payload,
})
}
})(coeffects)
```
## Component Configuration
### Common Components
| Component | Purpose | Key Properties |
| ----------------- | -------------- | --------------------- |
| `now-record-list` | Data table | columns, query, table |
| `now-record-form` | Record form | table, sysId, fields |
| `now-button` | Action button | label, variant, icon |
| `now-card` | Card container | header, content |
| `now-tabs` | Tab container | tabs, activeTab |
| `now-modal` | Modal dialog | opened, title |
### Record List Configuration
```json
{
"component": "now-record-list",
"properties": {
"table": "incident",
"query": "@state.filterQuery",
"columns": [
{ "field": "number", "label": "Number" },
{ "field": "short_description", "label": "Description" },
{ "field": "priority", "label": "Priority" },
{ "field": "state", "label": "State" },
{ "field": "assigned_to", "label": "Assigned To" }
],
"pageSize": 20,
"selectable": true,
"selectedRecords": "@state.selectedRecords"
}
}
```
### Form Configuration
```json
{
"component": "now-record-form",
"properties": {
"table": "incident",
"sysId": "@state.selectedIncident",
"fields": ["short_description", "description", "priority", "assignment_group", "assigned_to"],
"readOnly": false
}
}
```
## Macroponents
### Creating Reusable Macroponents
```
Macroponent: incident-summary-card
├── Properties (inputs)
│ ├── incidentSysId (string)
│ └── showActions (boolean)
├── Internal State
│ └── expanded (boolean)
├── Data Broker
│ └── incident_data (uses incidentSysId)
└── Layout
├── now-card
│ ├── Header: @data.incident.number
│ ├── Content: @data.incident.short_description
│ └── Footer: Action buttons
└── now-modal (if expanded)
```
### Macroponent Properties
```json
{
"properties": {
"incidentSysId": {
"type": "string",
"required": true,
"description": "Sys ID of incident to display"
},
"showActions": {
"type": "boolean",
"default": true,
"description": "Show action buttons"
},
"variant": {
"type": "string",
"default": "default",
"enum": ["default", "compact", "detailed"]
}
}
}
```
## MCP Tool Integration
### Available UIB Tools
| Tool | Purpose |
| ---------------------------------- | --------------------- |
| `snow_create_uib_page` | Create new page |
| `snow_create_uib_component` | Add component to page |
| `snow_create_uib_data_broker` | Create data broker |
| `snow_create_uib_client_state` | Define client state |
| `snow_create_uib_event` | Configure events |
| `snow_create_complete_workspace` | Full workspace |
| `snow_update_uib_page` | Modify page |
| `snow_validate_uib_page_structure` | Validate structure |
### Example Workflow
```javascript
// 1. Create workspace
await snow_create_complete_workspace({
name: "IT Support Workspace",
description: "Agent workspace for IT support",
landing_page: "incident_list",
})
// 2. Create data broker
await snow_create_uib_data_broker({
page_id: pageId,
name: "incident_list",
type: "graphql",
query: incidentQuery,
})
// 3. Add components
await snow_create_uib_component({
page_id: pageId,
component: "now-record-list",
properties: listConfig,
})
// 4. Configure events
await snow_create_uib_event({
page_id: pageId,
event_name: "NOW_RECORD_LIST#RECORD_SELECTED",
handlers: eventHandlers,
})
```
## Best Practices
1. **Use Data Brokers** - Never fetch data directly in components
2. **Client State for UI** - Use for filters, selections, view modes
3. **Events for Communication** - Decouple components via events
4. **Macroponents for Reuse** - Create reusable building blocks
5. **GraphQL for Queries** - More efficient than Script brokers
6. **Validate Structure** - Use validation tools before deployment
7. **Mobile Variants** - Create responsive variants
8. **Accessibility** - Follow WCAG guidelines
More from serac-labs/serac
- acl-securityCreate and debug ServiceNow ACLs (record, field, REST, script-include). Covers role/condition/script patterns, evaluation order, field-level visibility, and impersonation testing for row- and field-level security.
- agent-workspaceBuild ServiceNow Agent Workspace configurations — workspaces, lists, forms, contextual side panels, Agent Assist similar-record finders, and workspace-specific UI actions on sys_aw_* tables.
- approval-workflowsConfigure ServiceNow approval rules and sysapproval_approver records — manager/group/script approvers, multi-level routing, delegation via sys_user_delegate, and parent-record state rollup.
- asset-managementManage ServiceNow hardware assets, software licenses, and lifecycle states on alm_hardware/alm_license — license allocation, CMDB-to-asset linking, warranty tracking, inventory aggregation (HAM/SAM).
- atf-testingBuild ServiceNow Automated Test Framework tests and suites — impersonation, form steps, assertions, server-side script steps, test parameters, and execution via snow_create_atf_test / snow_execute_atf_test.
- blast-radiusTrace ServiceNow configuration dependencies — what artifacts touch a given field, what calls a script include, table/app-level config inventory. Use before deletes, renames, or refactors.
- bun-file-ioUse this when you are working on file operations like reading, writing, scanning, or deleting files. It summarizes the preferred file APIs and patterns used in this repo. It also notes when to use filesystem helpers for directories.
- business-rule-patternsWrite ServiceNow business rules (before/after/async/display) — current vs previous, changesTo/changesFrom, recursion avoidance, setAbortAction, and async dispatch for heavy work.
- catalog-itemsBuild ServiceNow Service Catalog items, variables, variable sets, catalog client scripts, record producers, and order guides with reference qualifiers and dynamic pricing.
- client-scriptsWrite ServiceNow client scripts (onLoad/onChange/onSubmit/onCellEdit) using g_form, g_user, GlideAjax, field visibility/mandatory toggles, and validation with debounced server calls.