@servicenow/sdk
Version:
ServiceNow SDK
753 lines (605 loc) • 23.7 kB
Markdown
---
tags: [view, view rule, list, list control, relationship, related list, form layout, sys_ui_view, sysrule_view, sys_ui_list, sys_ui_list_control, sys_relationship]
---
# Platform Views, Lists & Relationships
Guide for configuring ServiceNow views, view rules, lists, list controls, and relationships using the Fluent API.
---
## Views (sys_ui_view)
Views define which fields, sections, and layout appear on a form or list for a given table. A view definition alone is non-functional -- it must be combined with form/list components.
### Choosing the View Type
| If User Says | View Type | Action |
|--------------|-----------|--------|
| "simple", "basic", "default", "standard" | **Default** | `import { default_view } from '@servicenow/sdk/core'` -- no Record needed |
| "admins", "managers", "ITIL", "role" | **Role-based** | Set `roles` array |
| "team", "department", "group" | **Group-based** | Set `group` reference |
| "portal", "mobile app", "API", "hidden" | **Hidden** | Set `hidden: true` |
| Named individual ("John", "Dr. Smith") | **User-specific** | Set `user` reference |
| "everyone", "all users", "no restrictions" | **Public** | Omit access control fields |
### CRITICAL: Uniqueness Check
Both `name` and `title` must each be globally unique across all scopes. Query before creating:
```
table: sys_ui_view
query: name=<proposed_name>^ORtitle=<proposed_title>
```
If results > 0, change **both** name and title and re-query. Scope prefixes do not guarantee uniqueness.
### UI View Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `$id` | `Now.ID[string]` | Yes | Unique identifier |
| `table` | `string` | Yes | Must be `"sys_ui_view"` |
| `data.name` | `string` | Yes | Unique technical name (max 80) |
| `data.title` | `string` | Yes | Unique display name (max 80) |
| `data.roles` | `string[]` | No | Array of role name strings |
| `data.user` | `string` | No | sys_id or reference to sys_user |
| `data.group` | `string` | No | sys_id or reference to sys_user_group |
| `data.hidden` | `boolean` | No | Hide from platform view selector |
### View Examples
#### Public view
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const mobileView = Record({
$id: Now.ID['mobile-view'],
table: 'sys_ui_view',
data: {
name: 'incident_mobile',
title: 'Mobile View',
},
});
```
#### Role-based view
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const adminView = Record({
$id: Now.ID['admin-view'],
table: 'sys_ui_view',
data: {
name: 'incident_admin',
title: 'Admin View',
roles: ['admin'],
},
});
```
#### Group-based view
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const supportGroup = Record({
$id: Now.ID['support-group'],
table: 'sys_user_group',
data: { name: 'support_team', description: 'Support Team', active: true },
});
export const supportView = Record({
$id: Now.ID['support-view'],
table: 'sys_ui_view',
data: {
name: 'incident_support_team',
title: 'Support Team View',
group: supportGroup,
},
});
```
#### Hidden view (Portal/API)
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const portalView = Record({
$id: Now.ID['portal-view'],
table: 'sys_ui_view',
data: {
name: 'sp_incident_customer',
title: 'Customer Portal View',
hidden: true,
},
});
```
#### Using default_view
```typescript fluent
import { default_view } from '@servicenow/sdk/core';
import { Record } from '@servicenow/sdk/core';
export const section = Record({
$id: Now.ID['disaster-report-section'],
table: 'sys_ui_section',
data: {
name: 'u_disaster_report',
view: default_view,
},
});
```
### Forms Integration with Views
**Hierarchy:** UI View -> Form -> Sections -> Elements (fields/formatters/related lists)
A form requires explicit linking between forms and sections using the `sys_ui_form_section` join table. **Without `sys_ui_form_section` records, your form will appear EMPTY.**
#### Complete form with multiple sections
```typescript fluent
import { Record } from '@servicenow/sdk/core';
import { managerView } from './views';
// 1. Create Form
export const managerForm = Record({
$id: Now.ID['manager-form'],
table: 'sys_ui_form',
data: { name: 'incident', view: managerView, active: true },
});
// 2. Create Sections
export const detailsSection = Record({
$id: Now.ID['details-section'],
table: 'sys_ui_section',
data: { name: 'incident', view: managerView, caption: 'Case Details', position: 0 },
});
export const assignmentSection = Record({
$id: Now.ID['assignment-section'],
table: 'sys_ui_section',
data: { name: 'incident', view: managerView, caption: 'Assignment', position: 1 },
});
// 3. CRITICAL: Link Sections to Form
export const formDetailsLink = Record({
$id: Now.ID['form-details-link'],
table: 'sys_ui_form_section',
data: { sys_ui_form: managerForm, sys_ui_section: detailsSection, position: 0 },
});
export const formAssignmentLink = Record({
$id: Now.ID['form-assignment-link'],
table: 'sys_ui_form_section',
data: { sys_ui_form: managerForm, sys_ui_section: assignmentSection, position: 1 },
});
// 4. Add Fields to Sections
export const numberField = Record({
$id: Now.ID['number-field'],
table: 'sys_ui_element',
data: { element: 'number', sys_ui_section: detailsSection, position: 0, type: 'element' },
});
export const assignedToField = Record({
$id: Now.ID['assigned-to-field'],
table: 'sys_ui_element',
data: { element: 'assigned_to', sys_ui_section: assignmentSection, position: 0, type: 'element' },
});
```
#### Form element types
| Type | Description |
|------|-------------|
| `element` | Standard field |
| `formatter` | Custom formatter |
| `list` | Related list |
| `.begin_split` | Open 2-column area |
| `.split` | Column divider |
| `.end_split` | Close 2-column area |
| `.space` | Empty space |
#### Column layout with splits
Use splits for short fields (state, priority, category). Never split text areas (description, work_notes) or related lists -- these should be full width after `.end_split`.
```
.begin_split -> opens 2-column area
[left column fields]
.split -> column divider
[right column fields]
.end_split -> closes 2-column area
[full-width content: text areas, related lists]
```
**Positioning:** Use increments of 10 (0, 10, 20...) to allow easy insertion later.
---
## View Rules (sysrule_view)
View Rules automatically switch the form layout based on conditions, device type, or script logic. They require existing views -- views must exist in `sys_ui_view` first.
### Three Switching Approaches
1. **Device-Based:** Set `device_type` (`'mobile'`, `'tablet'`, `'browser'`)
2. **Condition-Based:** Set `condition` with encoded query (MUST end with `^EQ`)
3. **Script-Based:** Set `advanced: true` with custom `script`
### Evaluation Order
1. Active rules only (`active: true`)
2. Device type match
3. Condition satisfied
4. Script execution (if `advanced: true`)
5. First match wins
6. User preference (unless `overrides_user_preference: true`)
### CRITICAL: Encoded Query Requirements
- **Must end with `^EQ`** -- all encoded queries must terminate with `^EQ`.
- **Use backend field names** -- element names from `sys_dictionary`, not labels.
- **Use internal values** -- values from `sys_choice`, not display labels.
| WRONG | CORRECT | Why |
|-------|---------|-----|
| `Priority=1^EQ` | `priority=1^EQ` | Field name lowercase |
| `priority=Critical^EQ` | `priority=1^EQ` | Use internal value |
| `state=Closed^EQ` | `state=7^EQ` | Use state number |
| `priority=1` | `priority=1^EQ` | Must end with ^EQ |
### CRITICAL: One Advanced Rule Per Device Type
When multiple advanced (script-based) View Rules share the same table AND device_type, only the rule with the lowest `order` value is evaluated. Others are skipped. **Solution:** Combine all role/condition checks into a single script.
### View Rule Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `$id` | `Now.ID[string]` | Yes | Unique identifier |
| `table` | `string` | Yes | Must be `"sysrule_view"` |
| `data.name` | `string` | Yes | Descriptive name |
| `data.table` | `string` | Yes | Target table name |
| `data.view` | `string` | No | View name (from `sys_ui_view.name`, not title). Required unless using script |
| `data.condition` | `string` | No | Encoded query ending with `^EQ` |
| `data.device_type` | `string` | No | `'browser'`, `'mobile'`, or `'tablet'` |
| `data.active` | `boolean` | No | Default: `true` |
| `data.overrides_user_preference` | `boolean` | No | Override manual selection. Default: `true` |
| `data.advanced` | `boolean` | No | Enable custom script. Default: `false` |
| `data.script` | `string` | No | JavaScript logic (when `advanced: true`) |
| `data.order` | `number` | No | Evaluation order (lower first). Default: `100` |
### Advanced Script Variables
| Variable | Type | Availability | Description |
|----------|------|--------------|-------------|
| `view` | string | Always | Current view name |
| `is_list` | boolean | Always | true for lists, false for forms |
| `current` | GlideRecord | Forms only | Current record (undefined for lists) |
| `answer` | string/null | Always | Set to view name to switch |
| `gs` | GlideSystem | Always | GlideSystem API |
**Always check** `!is_list && typeof current !== 'undefined'` before accessing `current`.
### View Rule Examples
#### Device-based switching
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const mobileRule = Record({
$id: Now.ID['mobile-rule'],
table: 'sysrule_view',
data: {
name: 'Mobile View Rule',
table: 'incident',
view: 'mobile',
device_type: 'mobile',
active: true,
overrides_user_preference: true,
},
});
```
#### Condition-based switching
```typescript fluent
export const criticalRule = Record({
$id: Now.ID['critical-rule'],
table: 'sysrule_view',
data: {
name: 'Critical Priority Rule',
table: 'incident',
view: 'critical',
condition: 'priority=1^ORpriority=2^EQ',
active: true,
overrides_user_preference: true,
},
});
```
#### Role-based switching (advanced script)
```typescript fluent
export const roleRule = Record({
$id: Now.ID['role-rule'],
table: 'sysrule_view',
data: {
name: 'Role-Based View Rule',
table: 'incident',
view: null,
advanced: true,
active: true,
overrides_user_preference: true,
script: `(function overrideView(view, is_list) {
var user = gs.getUser();
if (user.hasRole('admin')) {
answer = 'admin_view';
} else if (user.hasRole('manager')) {
answer = 'manager_view';
} else if (user.hasRole('agent')) {
answer = 'agent_view';
} else {
answer = 'ess';
}
})(view, is_list);`,
},
});
```
#### Complex conditional logic (forms only)
```typescript fluent
export const complexRule = Record({
$id: Now.ID['complex-rule'],
table: 'sysrule_view',
data: {
name: 'Complex Conditional Rule',
table: 'incident',
view: null,
advanced: true,
active: true,
overrides_user_preference: true,
script: `(function overrideView(view, is_list) {
if (!is_list && typeof current !== 'undefined') {
var priority = current.priority.toString();
var state = current.state.toString();
if (priority === '1' && state === '2') {
answer = 'critical_active_view';
} else if (priority === '1' && state === '7') {
answer = 'critical_closed_view';
} else {
answer = null;
}
}
})(view, is_list);`,
},
});
```
---
## Lists (sys_ui_list)
Use the `List` API from `@servicenow/sdk/core`. Requires `table`, `view`, and `columns`.
### List Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `table` | `string` | Yes | Table name for the list |
| `view` | Reference | Yes | UI view variable or `default_view` |
| `columns` | `array` | Yes | List of ListElement objects (or string shorthand) |
| `parent` | `TableName` | No | Parent table for related lists |
| `relationship` | `sys_relationship` | No | Custom relationship for related lists |
| `$meta` | `object` | No | Installation metadata (`demo`, `first install`) |
### List Element Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `element` | `string` | Yes | Field name (supports dot-walking, e.g., `"caller_id.name"`) |
| `position` | `number` | No | Display position (defaults to array order) |
| `sum` | `boolean` | No | Show sum aggregate |
| `averageValue` | `boolean` | No | Show average aggregate |
| `minValue` | `boolean` | No | Show minimum aggregate |
| `maxValue` | `boolean` | No | Show maximum aggregate |
### List Examples
#### Basic list with column objects
```typescript fluent
import { List } from '@servicenow/sdk/core';
const serverList = List({
table: 'cmdb_ci_server',
view: app_task_view,
columns: [
{ element: 'name', position: 0 },
{ element: 'business_unit', position: 1 },
{ element: 'vendor', position: 2 },
{ element: 'cpu_type', position: 3 },
],
});
```
#### Related list with simple reference
When a simple reference field exists (e.g., `table.field`), no relationship sys_id is needed:
```typescript fluent
import { List, default_view } from '@servicenow/sdk/core';
List({
table: 'project_task',
view: default_view,
parent: 'project',
columns: ['assigned_to', 'short_description', 'due_date', 'state'],
});
```
#### Related list with explicit relationship
When no simple reference field exists, import the relationship and reference it:
```typescript fluent
import { List, default_view } from '@servicenow/sdk/core';
import { skillMatchedPlayersRelationship } from '../relationships/game_allotment_relationships.now';
export const skillList = List({
table: 'sn_sportshub_players',
view: default_view,
parent: 'sn_sportshub_sports',
relationship: skillMatchedPlayersRelationship,
columns: [
{ element: 'first_name', position: 0 },
{ element: 'gender', position: 1 },
{ element: 'email', position: 2 },
{ element: 'skill_level', position: 3 },
],
});
```
---
## List Controls (sys_ui_list_control)
List Controls configure UI options on table lists and related lists -- role-based New/Edit button visibility, disable pagination, conditional button hiding.
### Key Guidance
1. Each list control needs a unique `$id`, `table: 'sys_ui_list_control'`, and valid `name` (target table).
2. For related lists, use `related_list` in `table.field` or `REL:sys_id` format.
3. **Do not combine `omit_*_button: true` with `*_roles`** -- the omit flag overrides role permissions.
4. **Button visibility is OR logic**: hidden if `omit_*_button == true` OR `*_condition` evaluates to true.
5. Use `omit_count: true` for large tables (>10,000 records) for performance.
6. Condition scripts use `Now.include()` for external files.
### List Control Properties
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `name` | `TableName` | Yes | | Target table name |
| `related_list` | `string` | No | | `table.field` or `REL:sys_id` format |
| `label` | `string` | No | | Display label for list |
| `omit_new_button` | `boolean` | No | `false` | Hide New button for everyone |
| `omit_edit_button` | `boolean` | No | `true` | Hide Edit button for everyone |
| `omit_links` | `boolean` | No | `false` | Hide reference links |
| `omit_drilldown_link` | `boolean` | No | `false` | Disable first-column drilldown link |
| `omit_filters` | `boolean` | No | `false` | Hide filters/breadcrumbs |
| `omit_if_empty` | `boolean` | No | `false` | Hide related list when empty |
| `omit_count` | `boolean` | No | `false` | Remove pagination count |
| `omit_related_list_count` | `boolean` | No | `false` | Remove related list count in Workspace |
| `new_roles` | `string[]` | No | | Roles that can see New button |
| `edit_roles` | `string[]` | No | | Roles that can see Edit button |
| `filter_roles` | `string[]` | No | | Roles that can see filters |
| `link_roles` | `string[]` | No | | Roles that can see links |
| `new_condition` | Script | No | | Condition script to hide New button |
| `edit_condition` | Script | No | | Condition script to hide Edit button |
| `list_edit_type` | `string` | No | | `'save_by_row'`, `'disabled'`, or omit for default |
| `list_edit_ref_qual_tag` | `string` | No | | Tag passed to reference qualifier scripts |
| `hierarchical_lists` | `boolean` | No | `false` | Enable hierarchical list display |
| `disable_nlq` | `boolean` | No | `false` | Disable Natural Language Query |
| `active` | `boolean` | No | `true` | Whether control is active |
### Condition Script Pattern
```javascript
var answer;
if (parent.state == 6 || parent.state == 7) {
answer = true; // hide button
} else {
answer = false; // show button
}
answer;
```
- `parent` provides access to parent record fields.
- `answer = true` hides the button; `answer = false` shows it.
### List Control Examples
#### Performance optimization for large table
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const auditControl = Record({
$id: Now.ID['audit-list-control'],
table: 'sys_ui_list_control',
data: {
name: 'sys_audit',
omit_count: true,
omit_related_list_count: true,
},
});
```
#### Role-based button access
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const roleControl = Record({
$id: Now.ID['role-based-access'],
table: 'sys_ui_list_control',
data: {
name: 'incident',
new_roles: ['admin', 'itil'],
edit_roles: ['admin'],
},
});
```
#### Conditional button hiding on related list
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const conditionalControl = Record({
$id: Now.ID['incident-conditional-button'],
table: 'sys_ui_list_control',
data: {
name: 'incident',
related_list: 'incident.parent_incident',
new_condition: Now.include('../scripts/hideForClosedIncident.js'),
edit_condition: Now.include('../scripts/hideForClosedIncident.js'),
},
});
```
#### Hide related list when empty
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const hideIfEmpty = Record({
$id: Now.ID['omit-if-empty'],
table: 'sys_ui_list_control',
data: {
name: 'incident',
related_list: 'incident.parent_incident',
omit_if_empty: true,
},
});
```
#### Filter and link role restrictions
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const filterRoles = Record({
$id: Now.ID['filter-role-control'],
table: 'sys_ui_list_control',
data: {
name: 'incident',
filter_roles: ['admin', 'report_viewer'],
link_roles: ['admin', 'itil', 'user'],
},
});
```
#### Disable list editing
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const disableEdit = Record({
$id: Now.ID['disable-list-edit'],
table: 'sys_ui_list_control',
data: {
name: 'x_snc_financial_records',
list_edit_type: 'disabled',
},
});
```
#### List control on custom relationship
```typescript fluent
import '@servicenow/sdk/global';
import { Record } from '@servicenow/sdk/core';
import { activeHighPriorityRelationship } from '../relationships/game_allotment_relationships.now';
export const customRelControl = Record({
$id: Now.ID['sports_table_list_control'],
table: 'sys_ui_list_control',
data: {
name: 'sn_sportshub_sports',
related_list: `REL:${activeHighPriorityRelationship.$id}`,
omit_related_list_count: 'true',
},
});
```
---
## Relationships (sys_relationship)
Relationships define how tables relate for related lists and cross-table queries.
### Relationship Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `name` | `string` | No | Descriptive name |
| `basic_apply_to` | `TableName` | No | Table where relationship is defined (basic) |
| `basic_query_from` | `TableName` | No | Table being referenced (basic) |
| `reference_field` | `FieldName` | No | Field containing the reference |
| `query_with` | Script | No | Script to refine the query |
| `advanced` | `boolean` | No | Whether this is an advanced relationship |
| `simple_reference` | `boolean` | No | Whether this is a simple reference |
| `apply_to` | Script | No | Script for advanced: which table applies |
| `query_from` | Script | No | Script for advanced: which table to query |
Either use basic fields (`basic_apply_to`, `basic_query_from`) or advanced fields (`apply_to`, `query_from`) -- never both.
### Related List Configuration
Related lists use two tables:
- `sys_ui_related_list` -- container for a table's related lists in a view
- `sys_ui_related_list_entry` -- individual entries linking to relationships
For referential relationships: use `table.reference_field` format in the entry.
For non-referential relationships: use `REL:<relationship_sys_id>` format.
### Relationship Examples
#### Basic relationship between custom tables
```typescript fluent
import { Record } from '@servicenow/sdk/core';
export const deptAllocation = Record({
$id: Now.ID['department_rel_id'],
table: 'sys_relationship',
data: {
advanced: false,
basic_apply_to: 'sn_foo_department',
basic_query_from: 'sn_foo_student',
name: 'Department Allocation Relationship',
query_with: `(function refineQuery(current, parent) {
current.addQuery('department', parent.id);
})(current, parent);`,
simple_reference: false,
},
});
```
#### Related list container with entries
```typescript fluent
import { Record } from '@servicenow/sdk/core';
const deptRelatedList = Record({
$id: Now.ID['department_related_list_id'],
table: 'sys_ui_related_list',
data: {
calculated_name: 'Department - Default view',
name: 'sn_foo_department',
view: 'Default view',
},
});
Record({
$id: Now.ID['department_related_list_entry_id'],
table: 'sys_ui_related_list_entry',
data: {
list_id: deptRelatedList.$id,
position: '0',
related_list: `REL:${deptAllocation.$id}`,
},
});
```
#### Multiple related lists on one table
```typescript fluent
const productContainer = Record({
$id: Now.ID['products_related_lists'],
table: 'sys_ui_related_list',
data: { name: 'sn_product_life_products', view: 'Default view' },
});
Record({
$id: Now.ID['feature_requests_entry'],
table: 'sys_ui_related_list_entry',
data: {
list_id: productContainer.$id,
position: 0,
related_list: 'feature_requests.product',
},
});
Record({
$id: Now.ID['testing_reports_entry'],
table: 'sys_ui_related_list_entry',
data: {
list_id: productContainer.$id,
position: 1,
related_list: 'testing_reports.product',
},
});
```