Service Portal Development: Building Custom Widgets in ServiceNow

Service Portal is ServiceNow's end-user facing portal framework built on AngularJS 1.x. Custom widgets let you build interactive UI components that connect to ServiceNow data. This guide covers widget structure, the server/client data flow, calling the server from the client, and the patterns used in production.

Widget structure

Every Service Portal widget has four parts:

  • HTML Template — the rendered markup using AngularJS binding syntax
  • Client Controller — AngularJS controller for client-side logic and UI interactions
  • Server Script — runs on the server when the widget loads, populates the data object
  • CSS/SCSS — widget-specific styles scoped to this widget

Data flow: server to client on load

The server script runs first when the widget loads. It populates the data object. This data is then available in the client controller via c.data:

// Server Script — runs on load
(function() {
    data.incidents = [];
    var gr = new GlideRecord('incident');
    gr.addEncodedQuery('active=true^assigned_to=javascript:gs.getUserID()');
    gr.orderBy('priority');
    gr.setLimit(10);
    gr.query();
    while (gr.next()) {
        data.incidents.push({
            sys_id: gr.getUniqueValue(),
            number: gr.getValue('number'),
            short_description: gr.getValue('short_description'),
            state: gr.getDisplayValue('state'),
            priority: gr.getValue('priority')
        });
    }
    data.count = data.incidents.length;
})();

// HTML Template — AngularJS bindings
// <div>
//   <p>You have {{c.data.count}} open incidents</p>
//   <div ng-repeat="inc in c.data.incidents">
//     <strong>{{inc.number}}</strong> — {{inc.short_description}}
//   </div>
// </div>

Client-to-server communication

To call the server from the client (after initial load), use c.server.get():

// Client Controller
api.controller = function($scope) {
    var c = this;
    c.refreshData = function(filter) {
        c.server.get({action: 'refresh', filter: filter})
            .then(function(response) {
                c.data.incidents = response.data.incidents;
                c.data.count = response.data.count;
            });
    };
};

// Server Script — handle the action
(function() {
    if (input.action == 'refresh') {
        data.incidents = [];
        var gr = new GlideRecord('incident');
        gr.addEncodedQuery('active=true^state=' + input.filter);
        gr.setLimit(10);
        gr.query();
        while (gr.next()) {
            data.incidents.push({
                number: gr.getValue('number'),
                state: gr.getDisplayValue('state')
            });
        }
        data.count = data.incidents.length;
    }
})();

AngularJS binding syntax

<!-- Display a value -->
{{c.data.fieldName}}

<!-- Loop over an array -->
<div ng-repeat="item in c.data.items">{{item.name}}</div>

<!-- Show/hide based on condition -->
<div ng-if="c.data.showSection">...</div>

<!-- Click handler -->
<button ng-click="c.handleClick(item)">Action</button>

<!-- CSS class binding -->
<div ng-class="{'active': c.data.isActive}">...</div>

Service Portal vs Next Experience

Service Portal (AngularJS) is the established framework with the largest library of existing widgets and community examples. Next Experience (UIB — UI Builder) is the newer React-based framework ServiceNow is investing in going forward. For new portal development in 2026, check whether your instance supports Next Experience and whether it fits your use case before committing to Service Portal widgets.

Related guides: GlideRecord performance — writing efficient server scripts · Client Scripts — the non-portal equivalent · GlideAjax — similar client/server pattern

Widget anatomy — server script, client controller, HTML template

Every Service Portal widget has three components that run in different contexts. The Server Script runs on the server when the widget loads, populates the data object with everything the widget needs, and has full access to GlideRecord and the ServiceNow server APIs. The Client Controller is an AngularJS controller running in the browser — it reads from data, handles user interactions, and calls GlideAjax for dynamic server requests. The HTML Template uses AngularJS binding syntax ({{c.data.fieldName}}) to render data into UI.

// Server Script — runs on load
(function() {
    var inc = new GlideRecord('incident');
    inc.addEncodedQuery('caller_id=' + gs.getUserID() + '^active=true');
    inc.addFieldToSelect(['number','short_description','state','priority']);
    inc.orderByDesc('opened_at');
    inc.setLimit(5);
    inc.query();
    data.incidents = [];
    while (inc.next()) {
        data.incidents.push({
            number: inc.getValue('number'),
            title: inc.getValue('short_description'),
            state: inc.getDisplayValue('state'),
            priority: inc.getDisplayValue('priority')
        });
    }
})();

// HTML Template
<ul>
  <li ng-repeat="inc in c.data.incidents">
    {{inc.number}} — {{inc.title}} ({{inc.state}})
  </li>
</ul>

Dynamic widget interactions with spModal and GlideAjax

For actions after the initial load — a user submitting a form, clicking a button, requesting updated data — the client controller calls GlideAjax to a Script Include, receives a JSON response, and updates c.data to trigger Angular's two-way binding and re-render the template. ServiceNow's spModal service handles modal dialogs inside the portal without a page navigation — useful for confirmation dialogs, inline forms, and detail views that should not leave the current page context.

Widget CSS and Bootstrap integration

Service Portal uses Bootstrap 3 for layout. Widget CSS is scoped to the widget — styles you define in the CSS field only apply within that widget's DOM, not globally. Use Bootstrap grid classes (col-sm-6, col-md-4) for responsive layouts. For custom styling that needs to apply globally across a portal theme, use the Portal Theme's CSS variables rather than widget-level CSS. Inline styles in the HTML template should be avoided — they are hard to maintain and cannot be overridden by theme variables.

Performance considerations for widgets

Heavy server scripts on widgets that load on every page (header widgets, navigation widgets, dashboard widgets) affect every page load for every user. Apply the same discipline as any server-side script: use GlideRecord with setLimit and addFieldToSelect, use GlideAggregate for counts, and cache data that changes infrequently. A widget that queries 200 records on every page load multiplied by 500 concurrent users is a guaranteed performance problem.

Related: GlideAjax · Script Includes · Client Scripts · GlideRecord performance · ACLs — portal security model

Widget security — what developers miss

Service Portal widgets are accessible to portal users, who may have different (usually more restricted) access than internal users. The widget server script runs as the logged-in user's context — if a portal user without the itil role tries to view incident data through a widget that queries the incident table, the GlideRecord query returns no results because the user's ACLs deny access. This is correct behaviour — do not try to bypass it by running queries as a privileged user just to make the widget work. Instead, design portal widgets to surface only the data the portal user is legitimately allowed to see, using the same ACL framework that governs all platform access.

For anonymous portal widgets (accessible without login), the server script runs in the guest user context. Any data returned by an anonymous widget is publicly accessible data — never return internal records, sys_ids of sensitive records, or any information that should not be publicly visible. Design anonymous widgets conservatively; authenticated widgets designed for self-service users slightly less conservatively; internal portal widgets for IT staff at the same level as standard form access.

Testing and debugging widgets

The Service Portal Widget Editor (accessible via sp_config or the Widget Editor application) provides real-time preview of widget changes. Server script errors appear in the browser console and the System Log. Use console.log() in the client controller for client-side debugging. For server script debugging, add data.debug = { query: 'your debug info' } and inspect it in the browser using the Angular scope inspector or by temporarily rendering it in the template. The most common bugs are: the server script throws an error (check System Log), an Angular scope variable is undefined (check data object population in server script), or a GlideAjax call returns null (check that the Script Include is client-callable and the method name matches exactly).

Mobile responsiveness and accessibility

Service Portal is used on mobile devices, particularly in field service and HR self-service use cases. For the field service side specifically, see our FSM guide. Widgets that use Bootstrap's grid system (col-xs-12 col-sm-6 col-md-4) respond to screen size automatically. Avoid fixed-width layouts that assume desktop viewports. For widgets with forms, ensure tap targets are large enough for touch interaction (minimum 44px), input fields are appropriately sized, and keyboard handling works correctly for mobile users. Accessibility matters too — use semantic HTML (proper heading hierarchy, label elements for inputs, alt text for any images), ensure colour contrast meets WCAG AA standards (particularly important on a dark-themed portal), and test with a screen reader if your portal serves users who rely on assistive technology.

Widget caching strategies

Service Portal has built-in server-side caching for widgets that renders HTML once and serves it from cache for subsequent requests within the cache TTL. Enable caching on the Widget record (Cache field) for widgets whose output does not change per user — informational widgets, FAQ lists, static content widgets. Disable caching for widgets that show user-specific data or real-time information. When caching is enabled, the server script runs once per cache window rather than once per page load, which can dramatically reduce database load for heavily-visited portal pages. The cache key includes the widget sys_id and any instance options — widgets with different options configurations maintain separate caches.

Widget Security: What the Server Script Can Access

The server script in a Service Portal widget runs in the context of the logged-in portal user. This means ACLs are enforced automatically — if the user does not have permission to read a record, the GlideRecord query in the server script returns no results, not an error. This is correct behaviour but can cause confusion during development if you test as an admin (who has access to everything) and then deploy to users who have restricted access. Always test widgets with an impersonated restricted user before deployment. For portals with anonymous access (where users are not logged in), the server script runs as the guest user — any data returned to anonymous users is effectively public, regardless of ACLs. Review every widget's server script data output through this lens when building public-facing portals.

Performance: Client vs Server Data Loading

Widget performance is primarily determined by how much work the server script does and how much data it returns to the client. Server scripts that perform multiple GlideRecord queries, especially queries inside loops (the N+1 pattern), create slow widget load times that frustrate users. Apply the same GlideRecord performance discipline to widget server scripts as to Business Rules. Use GlideAggregate when the client only needs counts or sums, not individual records. Use GlideAjax for data that only loads on user interaction (button clicks, filter changes) rather than loading everything on initial page render. A widget that loads its core data on render and lazy-loads supplementary data on demand provides a much better user experience than one that blocks on a comprehensive data load before rendering anything.

50 scripting patterns in one guide

The NowSpectrum Pro Tips and Tricks guide covers scripting patterns including portal development, GlideRecord, and performance.

Get the Pro Tips Guide →
← Back to all posts