Ehab Pilloor
Giga Sage

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.

EhabPilloor_1-1794734471353.pngThe 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

EhabPilloor_2-1794734511913.pngFlow: 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.


Both buttons appear only on the related list built on the incident's Parent field, so that list must be on the form.

  1. Open any RITM in the classic UI.
  2. Right-click the form header → Configure → Related Lists.
  3. Switch the view to the one SOW uses (for RITMs this is usually the Service Operations Workspace view).
  4. 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.


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"
}

EhabPilloor_3-1794734624950.pngThe 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

EhabPilloor_4-1794734678507.pngThe 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.

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).

EhabPilloor_5-1794734725467.pngThe 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"
}

EhabPilloor_6-1794734781279.pngThe 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.

EhabPilloor_7-1794734802579.pngThe 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.

EhabPilloor_8-1794734861262.pngLink Incident opens the popup. Note the locked filters: only incidents with an empty Parent are offered.

EhabPilloor_9-1794734884994.pngAfter Add, both incidents appear in the related list.

EhabPilloor_10-1794734921763.pngRemove asks for confirmation first.

EhabPilloor_11-1794734945605.pngEach 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 as parentFieldName in the payload template, and replace parent with 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_project and dmn_demand tables. 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.
Version history
Last update:
14m ago
Updated by:
Contributors