Creating a Service Portal Process Flow Formatter in ServiceNow
Adding a Process Flow Formatter to the Service Portal in ServiceNow.

Overview
UI16 Equivalent
Adding a Process Flow Formatter to records in UI16 is simple enough, you right click the top banner of your table/record, go to Form Design, from there you drag over the Fields Object "Process Flow (Formatter)", save, and you are good to go.


Service Portal
But what if we want the same thing on the Service Portal? Unfortunately, in many cases, we have to create it from scratch as there does not appear to be an out of the box version.
In an effort to not have to build this from scratch for every implementation I do, I have made a generalised version below that you can use to get started. It should work on at least any Task extended table and should be easy to extend for any other table you can imagine.
If you are only working with Task extended tables, its as simple as creating the widget once and then dropping that widget into the page design of any Service Portal page you want it to show up on (with the caveat that the page must be using URL navigation and the Parameters "table" and "sys_id", example below).
dev12345.service-now.com/sp?id=ticket&table=incident&sys_id=f12ca184735123002728660c4cf6a7ef
Below is the basic code set up for your widget, I won't dig too deep into widget creation as I have covered that elsewhere, but for those that know how to make and deploy widgets, code and examples below.
Widget
Rundown
The Widget uses very little HTML, a fairly simple Server Script and the rest is CSS. The CSS has been mostly lifted and modified from the out of the box Process Flow Formatter, to try and keep it looking along the same theme as UI16, you can update this as needed to make it suit your environment.
In the Server Script there is an Else block with a simple hardcoded data object that shows a basic example of what is required, that being a list of objects that have a name to display, and a state value for the chevron (either past, active or future).
Above this example block is a code segment that should work for Task extended tables.
Happy coding!
Code
<div>
<ol class="process-flow-container" id="processFlow">
<li class="process-step" ng-repeat="state in data.states" ng-class="{'state-past':state.state=='past','state-active':state.state=='active','state-future':state.state=='future'}"><a role="presentation" aria-disabled="true">{{state.name}}</a></li>
</ol>
</div>
HTML Template
::after {
box-sizing: border-box;
}
.process-flow-container {
border: none;
display: flex;
height: auto;
list-style: none;
margin-block-start: 0;
margin-block-end: 14px;
min-height: 46px;
overflow-x: auto !important;
overflow-y: hidden !important;
padding: 0;
white-space: nowrap;
width: 100%;
overflow:visible;
}
.process-flow-container li {
display: inline-flex;
position: relative;
flex: 1 1 auto;
}
.process-flow-container li a {
align-items: center;
background-color: rgb(228, 230, 234);
color: rgb(21, 25, 32);
display: inline-flex;
font-size: 14px;
height: 100%;
justify-content: center;
outline: 0;
overflow: hidden;
padding: 0;
padding-inline-start: 28px;
padding-inline-end: 10px;
text-decoration: none;
text-overflow: ellipsis;
transition: none;
white-space: nowrap;
width: 100%;
}
.process-flow-container li:not(:last-child)::after {
background: LINEAR-GRADIENT(45deg, rgba(0, 0, 0, 0) 50%, rgb(228, 230, 234) 50%);
border-block-start: 2px solid rgb(255, 255, 255);
border-inline-end: 2px solid rgb(255, 255, 255);
border-radius: 4px;
bottom: 0;
content: "";
height: 100%;
margin-block-start: -23px;
pointer-events: none;
position: absolute;
right: -14px;
top: 50%;
transform: rotate(45deg);
width: 46px;
z-index: 1;
}
/* Past states */
.state-past {
cursor: default;
background-color: rgb(228, 230, 234) !important;
color: rgb(21, 25, 32) !important;
}
.state-past::after {
background: linear-gradient(45deg, rgba(0, 0, 0, 0) 50%, rgb(228, 230, 234) 50%) !important;
}
.state-past a::after {
font-family: now-icons;
display: inline-flex;
line-height: 1;
font-weight: 400;
font-style: normal;
speak: none;
text-decoration: inherit;
text-transform: none;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
content: "\f12e";
color: rgba(0, 0, 0, 0);
vertical-align: text-bottom;
margin-inline-start: 4px;
}
/* Active state */
.state-active a {
background-color: rgb(0, 123, 88) !important;
color: rgb(255, 255, 255) !important;
}
.state-active::after {
background: linear-gradient(45deg, rgba(0, 0, 0, 0) 50%, rgb(0, 123, 88) 50%) !important;
color: rgb(21, 25, 32) !important;
}
/* Future states */
.state-future {
cursor: default;
background-color: rgb(228, 230, 234) !important;
}
.state-future::after {
background: LINEAR-GRADIENT(45deg, rgba(0, 0, 0, 0) 50%, RGB(var(--now-color_background--tertiary, var(--now-color--neutral-2, 228, 230, 231))) 50%) !important;
}
CSS - SCSS
(function() {
/* populate the 'data' object */
/* e.g., data.table = $sp.getValue('table'); */
data.states = [];
// Get our data from the URL
data.table = $sp.getParameter('table');
data.sys_id = $sp.getParameter('sys_id');
// Get Record
var record = new GlideRecord(data.table);
if(record.get(data.sys_id)){
data.record = record;
}
// Only do something if we have a record
if(data.record){
// Check if table extends from something
data.extendedParent = '';
var dbo = new GlideRecord('sys_db_object');
if(dbo.get('name',data.table)){
var superClass = dbo.getValue('super_class');
// Get the name of our super class, not a sysid
var sc = new GlideRecord('sys_db_object');
if(sc.get(superClass)){
data.extendedParent = sc.name;
}
}
// If table extends task, map to state
if(data.extendedParent == 'task'){
var states = new GlideRecord('sys_choice');
states.addQuery('name',data.table); // Use the table specific state values
states.addQuery('element','state');
states.addQuery('language','en');
states.addQuery('inactive',false);
states.orderBy('sequence');
states.query();
while(states.next()){
// Map state value to formatter states
var stateValue = '';
switch(true){
case data.record.getValue('state') == states.getValue('value'):
stateValue = 'active';break;
case data.record.getValue('state') < states.getValue('value'):
stateValue = 'future';break;
case data.record.getValue('state') > states.getValue('value'):
stateValue = 'past';break;
default:
stateValue = 'future';break;
}
data.states.push({'name':states.getValue('label'),'state':stateValue})
}
}else{
// We can expand our mappings from other tables here, example data provided
data.states = [
{"name":"Step 1","state":"past"},
{"name":"Step 2","state":"past"},
{"name":"Step 3","state":"active"},
{"name":"Step 4","state":"future"},
{"name":"Step 5","state":"future"}
];
}
}
})();
Server Script
Examples of the Widget deployed to the Standard Ticket Page



Comments ()