- Post History
- Subscribe to RSS Feed
- Mark as New
- Mark as Read
- Bookmark
- Subscribe
- Printer Friendly Page
- Report Inappropriate Content
13m ago
Disclosure: I used Claude (Anthropic's AI assistant) to help build this solution and write this article. The solution is designed using out-of-the-box records as the reference: the OOB Add button on the Approvers related list (its declarative action, payload definition and event mapping) and the OOB Add / Remove actions on child incidents. These buttons follow the same patterns. Everything was built and tested in my own PDI.
I'm sharing this because I could not find exact steps to follow for this, especially for someone who is not used to working in SOW. I hope it helps others building something similar, and that they can use this article as a reference.
In Service Operations Workspace (SOW) you can create a new incident from a requested item's related list, but there is no out-of-the-box way to link an existing incident to the RITM, or to remove it again.
This article builds two buttons on the Incidents related list of a RITM, using only platform configuration:
| Button | What the agent does | What happens |
|---|---|---|
| Link Incident | Clicks it, ticks one or more incidents in a popup, clicks Add | Each ticked incident's Parent is set to the RITM |
| Remove (n) | Ticks rows in the related list, clicks Remove, confirms | Each ticked incident's Parent is cleared |
The popup is ServiceNow's own multi-select list, the same one SOW uses for Add on the Approvers related list. Every record is explained field by field, so you can build the same pair of buttons for problems, changes, projects or any other table.
The finished buttons on a RITM's Incidents related list. Remove appears once a row is ticked.
How it fits together
Workspace buttons are not UI actions. On related lists in SOW they are declarative actions (sys_declarative_action_assignment). A declarative action decides where a button shows, who sees it, and what a click does.
The two buttons are built differently:
- Remove is one record: a declarative action whose click runs a short server script, once per ticked row.
- Link reuses ServiceNow's multi-record association (MRA) popup. A click does not run code; it raises an event, and three supporting records tell the platform which popup to open and what it should write.
| Step | Record | Table | Button | Job |
|---|---|---|---|---|
| 1 | Related list | sys_ui_related_list |
Both | Shows Incident → Parent on the RITM form |
| 2 | Payload definition | sys_declarative_action_payload_definition |
Link | Popup settings: which table to list, which columns, which field to fill |
| 3 | Payload mapping | sys_declarative_action_payload_mapping |
Link | The same settings for the Related List model. Created automatically |
| 4 | Declarative action | sys_declarative_action_assignment |
Link | The Link Incident button |
| 5 | UX add-on event mapping | sys_ux_addon_event_mapping |
Link | Turns the click into "open the popup" |
| 6 | Declarative action | sys_declarative_action_assignment |
Remove | The Remove (n) button and its script |
No script include, business rule or ACL is needed.
What happens when an agent uses the buttons
Flow: Link (left) and Remove (right) at run time.
Read each column top to bottom. Link never runs your code: the declarative action raises an event, the event mapping opens the popup with the settings from the payload definition, and the platform writes parent on Add. Remove runs its own server script, once for each ticked row.
Before you start
- Role: admin.
- Application scope: Global.
- Update set: create one and make it current before you create anything.
- The child has a reference to the parent. Here,
incident.parent(inherited from Task) points to the RITM. Every table that extends Task has it.
About names: the record names in this article (link_incident_to_ritm, LINK_INCIDENT_TO_RITM and so on) are examples. The screenshots use a different convention (rll_link_sc_req_item_incident, RLL_SC_REQ_ITEM_INCIDENT). Any unique name works, as long as each record points to the right one.
Step 1: Add the related list to the RITM form
Both buttons appear only on the related list built on the incident's Parent field, so that list must be on the form.
- Open any RITM in the classic UI.
- Right-click the form header → Configure → Related Lists.
- Switch the view to the one SOW uses (for RITMs this is usually the Service Operations Workspace view).
- Move Incident → Parent to the selected side and save.
For another table: add <Table> → Parent instead, for example Problem → Parent.
Note: the SOW view's related list record belongs to the Request Management for Service Operations Workspace scope. Capture it in an update set of that scope, or add the list by hand in each instance. A Global update set will refuse to commit it.
Link button
Step 2: Payload definition (the popup settings)
This record tells the MRA popup which table to list, which columns to show, and which field to fill on Add.
Open sys_declarative_action_payload_definition.list → New:
| Field | Value | What it does |
|---|---|---|
| Key | LINK_INCIDENT_TO_RITM |
Unique key, uppercase |
| Label | Link Incident to RITM | Name in the list |
| Applicable To | Related List | Lets related list buttons use it |
| Payload | the JSON below | The popup settings |
{
"label": "translate('Add')",
"table": "{{table}}",
"parentRecordSysId": "{{parentRecordSysId}}",
"relatedListName": "{{relatedListName}}",
"referencedFieldName": "sys_id",
"type": "o2m",
"extensionPoint": "",
"view": "Default",
"hideSelectAll": false,
"userGivenTable": "incident",
"parentFieldName": "parent",
"columns": "number,short_description,caller_id,state,assignment_group,opened_at"
}
The payload definition: Applicable To = Related List, and the popup settings in Payload.
What each key means:
| Key | Value here | Meaning | Change it for another table? |
|---|---|---|---|
type |
o2m |
One-to-many: write a value into a field on each selected record. (m2m, as on Approvers, creates rows in a link table instead.) |
No |
userGivenTable |
incident |
Table listed in the popup | Yes, the child table |
parentFieldName |
parent |
Field on each selected record that receives the parent | Only if the child uses another field |
referencedFieldName |
sys_id |
Which value of the parent is written | No |
columns |
number,short_description,… |
Popup columns, left to right; each gets a filter box. Field names, no spaces | Yes, fields of the child table |
label |
translate('Add') |
Text of the popup's confirm button | No |
extensionPoint |
empty | Optional pre-filter for the popup | No |
view, hideSelectAll |
Default, false |
Popup list view; show the select-all box | No |
{{table}}, {{parentRecordSysId}}, {{relatedListName}} |
placeholders | Filled in by the platform at runtime | No, keep exactly as is |
When you save, ServiceNow logs several Invalid query string received lines and creates one payload field record per key (sys_declarative_action_payload_field), with a label for each. That is normal; keep them in your update set.
Step 3: Payload mapping (created for you, just check it)
When the payload definition is saved, ServiceNow creates a matching payload mapping: the same settings as an encoded query, for the Related List model.
Open sys_declarative_action_payload_mapping.list, filter Action payload definition = Link Incident to RITM, and check:
| Field | Expected value |
|---|---|
| Label | Link Incident to RITM Related List |
| Action Payload Definition | Link Incident to RITM |
| Model | Related List |
| Map | label=translate('Add')^table={{table}}^parentRecordSysId={{parentRecordSysId}}^relatedListName={{relatedListName}}^referencedFieldName=sys_id^type=o2m^extensionPoint=^view=Default^hideSelectAll=false^userGivenTable=incident^parentFieldName=parent^columns=number,short_description,caller_id,state,assignment_group,opened_at^EQ |
The auto-created payload mapping: Model = Related List, and the same settings in Map.
If Map is empty or still shows old values, edit it to match the payload template and save.
For another table: nothing to do beyond checking that userGivenTable, parentFieldName and columns match that table's payload definition.
Step 4: The Link Incident button (declarative action)
Open sys_declarative_action_assignment.list → New:
| Field (as labelled on the form) | Value | What it does |
|---|---|---|
| Action label | Link Incident | Button text |
| Action name | link_incident_to_ritm |
Unique name |
| Implemented as | UXF Client Action | Raises an event instead of running code |
| Specify client action | Link Incident to RITM | The payload definition from Step 2 |
| Table | Incident [incident] |
The related list's table: the button sits above incident lists |
| Model | Related List | Places it above a related list. Not shown on every form view: open the list filtered on Model = Related List and click New, and the filter pre-fills it |
| Button type / Order | Primary / 100 | Blue, before Remove |
| Record Selection Required | false | Shows without ticking rows |
| Conditions tab: Script Condition | see below | Who sees it, and on which form |
| Conditions tab: Client Conditions | query is parent={{parentRecordSysId}} |
The model condition: only on the related list built on Parent |
| Conditions tab: Required user role names | itil | Role needed to see it |
| Conditions tab: Requires write access | true | User must be able to write the table (same as the OOB Add on child incidents) |
Script condition (runs on the server; parent is the RITM on the form, current is an empty incident record):
parent.getTableName() == 'sc_req_item' && parent.active == true && parent.canWrite() && current.canWrite() && current.parent.canWrite()
Why the button shows only on the RITM's Incidents list
Three settings work together, each one narrowing where the button can appear:
| Setting | Value | What it limits |
|---|---|---|
| Table + Model | Table = incident, Model = Related List |
Only above incident related lists, not on incident forms or lists. On its own, this would still show under a problem, a change or another incident |
| Script condition | parent.getTableName() == 'sc_req_item' && … |
Only when the form is a RITM. parent is the record on the form, so under any other parent the button is hidden |
| Model condition | query = parent={{parentRecordSysId}} |
Only the incident list built on the Parent field of this RITM. A second incident related list on the RITM, based on another field, would not get the button. The OOB Add on child incidents uses the same technique with parent_incident |
The rest of the script condition decides who and when, not where: the RITM must be active, and the user must be able to edit the RITM, incidents, and the incident's Parent field (field-level ACLs count). This is the same pattern ServiceNow uses for Remove on child incidents. Required user role names (itil) is a further gate on who.
Finally, the button has nowhere to appear unless the Incident → Parent related list is on the RITM form (Step 1).
The Link button: UXF Client Action pointing to the payload definition. On the Conditions tab, the script condition, the Client Conditions (model condition) query is parent={{parentRecordSysId}}, the role and Requires write access.
For another table: change Table, Action label, Action name, Specify client action (that table's payload definition) and the field in the condition and model condition if it is not parent. To put the buttons on a different parent form, change 'sc_req_item' in the condition.
Step 5: UX add-on event mapping (the click wiring)
A UXF client action only raises an event. This record catches it on the RITM's record page and opens the MRA popup in a modal, passing it the settings from Step 2. Without it, Link Incident shows but does nothing.
Open sys_ux_addon_event_mapping.list → New:
| Field (as labelled on the form) | Value | What it does |
|---|---|---|
| Event Mapping Name | SOW Related list Link incident to RITM | Name in the list |
| Source Declarative Action | link_incident_to_ritm | The button from Step 4 |
| Source Component | Related Records | The component that hosts related list buttons |
| Controller | Form | The SOW record page controller |
| Target Event | [Record Page] Open modal | Opens a modal on the record page |
| Target Payload Mapping | the JSON below | Passes every popup setting to the modal, route mra, size lg |
| Active | true |
{
"container": {
"fields": {
"container": {
"columns": { "binding": { "address": ["columns"] }, "type": "EVENT_PAYLOAD_BINDING" },
"extensionPoint": { "binding": { "address": ["extensionPoint"] }, "type": "EVENT_PAYLOAD_BINDING" },
"hideSelectAll": { "binding": { "address": ["hideSelectAll"] }, "type": "EVENT_PAYLOAD_BINDING" },
"label": { "binding": { "address": ["label"] }, "type": "EVENT_PAYLOAD_BINDING" },
"parentFieldName": { "binding": { "address": ["parentFieldName"] }, "type": "EVENT_PAYLOAD_BINDING" },
"parentRecordSysId": { "binding": { "address": ["parentRecordSysId"] }, "type": "EVENT_PAYLOAD_BINDING" },
"referencedFieldName": { "binding": { "address": ["referencedFieldName"] }, "type": "EVENT_PAYLOAD_BINDING" },
"relatedListName": { "binding": { "address": ["relatedListName"] }, "type": "EVENT_PAYLOAD_BINDING" },
"table": { "binding": { "address": ["table"] }, "type": "EVENT_PAYLOAD_BINDING" },
"userGivenTable": { "binding": { "address": ["userGivenTable"] }, "type": "EVENT_PAYLOAD_BINDING" },
"view": { "binding": { "address": ["view"] }, "type": "EVENT_PAYLOAD_BINDING" }
},
"type": "MAP_CONTAINER"
},
"params": {
"container": { "type": { "binding": { "address": ["type"] }, "type": "EVENT_PAYLOAD_BINDING" } },
"type": "MAP_CONTAINER"
},
"route": { "type": "JSON_LITERAL", "value": "mra" },
"size": { "type": "JSON_LITERAL", "value": "lg" }
},
"type": "MAP_CONTAINER"
}
The click wiring: Source Declarative Action = the Link button, Controller = Form, Source Component = Related Records, Target Event = [Record Page] Open modal.
Tip: some instances have two components named Related Records. Pick the one used by the OOB mapping SOW Related list Add approvers: open that record and copy its Controller, Source Component, Target Event and Target Payload Mapping into yours.
For another table: change only Event Mapping Name and Source Declarative Action. Everything else is identical for every Link button.
Remove button
Step 6: The Remove (n) button (declarative action)
One record. It follows the OOB Remove on child incidents (remove_child_incident), whose whole script is current.parent_incident = ''; current.update();. Ours adds safety checks and messages.
Open sys_declarative_action_assignment.list → New:
| Field (as labelled on the form) | Value | What it does |
|---|---|---|
| Action label | Remove ({{count}}) |
{{count}} shows how many rows are ticked |
| Action name | remove_incident_from_ritm |
Unique name |
| Implemented as | Server Script | Runs the script below on click |
| Table | Incident [incident] |
The related list's table |
| Model | Related List | As in Step 4 |
| Button type / Order | Secondary / 110 | Grey, after Link |
| Record Selection Required | true | Appears only when rows are ticked |
| Server Script | see below | Runs once per ticked row |
| Conditions: Script Condition | same as the Link button | The same users see both buttons |
| Conditions: Client Conditions | query is parent={{parentRecordSysId}} |
The same related list as Link |
| Conditions: Required user role names | itil | |
| Conditions: Requires write access | true | |
| Confirmation Settings: Confirmation required / message | true / Remove the selected records from this record? | Asks before removing |
Server script (current is the ticked incident, parent is the RITM on the form):
if (current.getValue('parent') == parent.getUniqueValue() && current.canWrite() && current.parent.canWrite()) {
current.autoSysFields(false); // keep Updated / Updated by unchanged
current.setValue('parent', '');
if (current.update())
gs.addInfoMessage(gs.getMessage('{0} removed.', [current.getDisplayValue()]));
else
gs.addErrorMessage(gs.getMessage('Unable to remove {0}.', [current.getDisplayValue()]));
} else {
gs.addErrorMessage(gs.getMessage('{0} is not linked to this record, or you cannot change it.', [current.getDisplayValue()]));
}
The checks re-confirm on the server that each row really is linked to this RITM and that the user may edit the record and its Parent field. autoSysFields(false) keeps the incident's Updated and Updated by unchanged, so removing a link does not look like an edit.
The Remove button: Server Script with Record Selection Required, the inline script, and the same conditions as Link.
For another table: change Table, Action label if you like, Action name, and replace parent in the script, condition and model condition if the child uses another field.
Test it
Hard-refresh SOW (Ctrl+Shift+R) and test as an itil user, not admin.
- An active RITM's Incidents related list shows Link Incident.
- Clicking it opens the multi-select popup with your columns.
- Ticking two incidents and clicking Add puts both in the related list.
- Ticking a row shows Remove (1); it asks for confirmation, then the row disappears.
- The removed incident's Updated by has not changed.
- The buttons do not show on a closed RITM, or on incident related lists under other parents.
Link Incident opens the popup. Note the locked filters: only incidents with an empty Parent are offered.
After Add, both incidents appear in the related list.
Remove asks for confirmation first.
Each removed row shows a message, and the list refreshes.
Apply it to other tables
Each new child table needs the same six steps (Step 3 is created for you). Most values stay the same; this is everything that changes, using Problem on a RITM as the example.
| Record | Field | Incident (this article) | Problem |
|---|---|---|---|
| Related list | Related list on the RITM form | Incident → Parent | Problem → Parent |
| Payload definition | Label / Key | Link Incident to RITM / LINK_INCIDENT_TO_RITM |
Link Problem to RITM / LINK_PROBLEM_TO_RITM |
userGivenTable |
incident |
problem |
|
columns |
number,short_description,caller_id,state,assignment_group,opened_at |
number,short_description,state,assignment_group,opened_at |
|
| Link button | Table / Action label / Action name | incident / Link Incident / link_incident_to_ritm |
problem / Link Problem / link_problem_to_ritm |
| Specify client action | Link Incident to RITM | Link Problem to RITM | |
| Event mapping | Event Mapping Name / Source Declarative Action | … incident … / link_incident_to_ritm |
… problem … / link_problem_to_ritm |
| Remove button | Table / Action name | incident / remove_incident_from_ritm |
problem / remove_problem_from_ritm |
Stays the same for every table: type, referencedFieldName, the {{…}} placeholders, the event mapping's Controller, Source Component, Target Event and Target Payload Mapping, the script condition, the model condition and the Remove script (as long as the child's field is parent).
Three variations:
- The child links through a different field (for example a custom
u_request😞 use it asparentFieldNamein the payload template, and replaceparentwith it in both script conditions, both model conditions and the Remove script. - A different parent form (for example linking incidents to a problem): change
'sc_req_item'in the script condition, and add the related list on that form instead. - The child does not extend Task: check it has a reference field to the parent, then treat it as the case above.
Popup columns for other common tables: change_request (number,short_description,type,state,assignment_group,start_date), pm_project (number,short_description,state,project_manager,start_date,end_date), dmn_demand (number,short_description,state,assigned_to,opened_at).
Notes and troubleshooting
Good to know
- Link updates Updated by, Remove does not. The MRA popup writes the records itself; the Remove script uses
autoSysFields(false). If the field is audited, history still shows who linked or removed. - The popup lists only records that are not linked yet. In o2m mode it adds locked filters automatically (Parent is empty, and the current record excluded), so incidents already linked to any parent do not appear. Agents narrow it further with the column filters.
- Classic UI needs nothing. Classic shows the OOB Edit… button on one-to-many related lists, which links and unlinks through the same Parent field.
- Tested on: Brazil Patch 1 Hotfix 1, in a Personal Developer Instance.
What goes in your update set
Per child table: 1 payload definition, 1 payload mapping, 2 declarative actions, 1 event mapping, plus the platform-generated payload fields (sys_declarative_action_payload_field) and their labels (sys_documentation), about 12 of each. The related list configuration for the SOW view belongs to the Request Management for Service Operations Workspace scope, so capture it separately or add it by hand.
Troubleshooting
| What you see | Likely cause | Fix |
|---|---|---|
| No buttons | SOW cached, wrong role, or script condition fails | Hard-refresh or log out and in; check itil and write access |
| Buttons on one related list only, or none on it | Model condition does not match that list's query | Use the child's Parent related list |
| Link shows, click does nothing | Event mapping missing, inactive, or wrong Source Declarative Action | Check Step 5 |
| Popup opens, Add links nothing | type is not o2m, or parentFieldName is wrong |
Check the payload template (Step 2) and mapping (Step 3) |
| Popup shows an empty column | A name in columns is not a field on that table |
Use field names from the table's dictionary |
| Remove says not linked to this record | The row is linked through another field | Expected; Remove clears only parent |
Update set preview: scope mismatch on sys_ui_related_sc_req_item_sow |
SOW view related list captured in a Global update set | Skip it in preview and add the related list by hand, or capture it in the SOW scope |
Attached update set: what it contains
The attached SOW_Link_Unlink_Buttons.xml is a ready-made version of everything in this article, for five tables on the RITM: incident, problem, change_request, pm_project and dmn_demand. It is a batch: one parent update set with two children. Import it, then preview and commit the parent, and the children come with it.
| Update set | Role | Contents |
|---|---|---|
| SOW_Link_Unlink_Buttons | Parent | No records of its own; groups the two below |
| Link buttons | Child | The original build: Link and Remove buttons, popup settings, popup mappings and click wiring for all five tables |
| Link Buttons V2 | Child | The five Remove buttons again, with the self-contained server script shown in Step 6. These are the newer versions, so they are the ones that end up in your instance |
For each of the five tables, it contains:
- a Link button (
rll_link_sc_req_item_<table>) and a Remove button (rll_remove_sc_req_item_<table>) - a payload definition (RLL Link <Table>), its payload mapping, and a UX add-on event mapping
- the 12 payload fields and 12 field labels the platform generates for the payload definition
It also contains 9 deletions of old popup fields from an earlier version of the popup settings. They are harmless: if those records do not exist in your instance, there is nothing to delete.
Not included:
- The RITM related lists. Add Incident / Problem / Change Request / Project / Demand → Parent to your RITM form yourself (Step 1). They are left out on purpose: the related list record holds the whole layout of a view, so importing it would overwrite your own RITM related lists, and the SOW view's record cannot be committed from a Global update set.
- Any script include, business rule or ACL. None is needed.
Before you commit:
- Import it in a sub-production instance first: System Update Sets → Retrieved Update Sets → Import Update Set from XML.
- The project and demand buttons need the
pm_projectanddmn_demandtables. If your instance does not have Project or Demand Management, skip those records in preview, or deactivate the two buttons afterwards. - Everything is in Global scope and was built on Brazil Patch 1 Hotfix 1. Record names use the
rll_/RLL_prefix shown in the screenshots.