ui-builder-patterns
SKILL.md
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
// 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)
// 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
// 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
// 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
// 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)
// 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
{
"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
{
"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
{
"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
// 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
- Use Data Brokers - Never fetch data directly in components
- Client State for UI - Use for filters, selections, view modes
- Events for Communication - Decouple components via events
- Macroponents for Reuse - Create reusable building blocks
- GraphQL for Queries - More efficient than Script brokers
- Validate Structure - Use validation tools before deployment
- Mobile Variants - Create responsive variants
- Accessibility - Follow WCAG guidelines
Weekly Installs
51
Repository
groeimetai/snow-flowGitHub Stars
53
First Seen
Jan 22, 2026
Security Audits
Installed on
claude-code46
github-copilot45
codex45
gemini-cli45
opencode45
cursor44