What It Is
Widgets are self-contained UI components that combine four distinct layers: an HTML template for markup structure, CSS for styling, a client-side AngularJS controller for browser-side logic, and a server-side script for data processing and ServiceNow API interactions. Each widget operates as an independent module that can be dropped onto Service Portal pages, configured through options, and communicate with other widgets via broadcast events. Unlike traditional ServiceNow UI pages that rely on Jelly templates and form layouts, widgets give you complete control over the presentation layer while maintaining secure server-side data access through the gs object and scoped GlideRecord queries.
Architecturally, widgets live in the Service Portal application as records in the sp_widget table, sitting between the presentation layer and the ServiceNow platform APIs. They execute within the Service Portal runtime environment, which provides the AngularJS framework, Bootstrap CSS library, and a secure bridge between client and server code through the spUtil service and server-side data object. The widget system operates as a managed runtime where ServiceNow handles the AngularJS application bootstrapping, dependency injection, and secure communication channels, while you focus on the business logic and user interface design within each widget's scope.
The widget system relates to ServiceNow's data model through server-side scripts that have full access to GlideRecord, GlideSystem, and all platform APIs, but with automatic scoping and security context applied. When a widget's server script executes, it runs in the context of the portal's configured user and application scope, with results passed to the client controller through the data object that becomes available as c.data in the AngularJS controller. This creates a clean separation where sensitive database operations happen server-side with proper ACL enforcement, while user interactions and dynamic UI updates happen client-side through AngularJS data binding and event handling. Widget options provide configuration flexibility, allowing the same widget code to behave differently based on instance-specific settings passed from the containing page or portal configuration.
You cannot function without widgets when building any custom Service Portal interface beyond basic CMS content pages. Every interactive element in Service Portal—from login forms and knowledge search to incident creation and approval workflows—requires widgets to handle the combination of data retrieval, user input validation, and dynamic content updates. Standard ServiceNow forms and lists don't exist in Service Portal; widgets replace them entirely, meaning any portal functionality that goes beyond static HTML content must be implemented through custom or out-of-box widgets. The business necessity becomes critical when you need to provide external users (customers, vendors, employees) with ServiceNow functionality through a modern, responsive web interface that can't be delivered through the platform UI's iframe-based embedding approach.
Widget development and management spans multiple roles with distinct responsibilities. ServiceNow developers create and maintain widget code, handling the HTML templates, AngularJS controllers, server scripts, and CSS styling that define widget functionality. Portal administrators configure widget instances on pages, set widget options, and manage page layouts without touching the underlying code. Application administrators control widget visibility, security, and application scope assignments that determine which portals can use specific widgets. In practice, the same person often wears multiple hats, but the platform enforces clear boundaries: widget records require appropriate application scope access to modify, while widget instance configuration on pages only requires portal administration rights.
Recent ServiceNow releases have maintained widget functionality largely unchanged, as the focus has shifted toward Next Experience and Workspace development for internal users. However, Vancouver introduced improved Studio integration for widget development, and Xanadu added better debugging capabilities for AngularJS controllers through enhanced browser developer tools integration. The significant change is strategic: while widgets remain fully supported for Service Portal, new development effort from ServiceNow focuses on UI Builder components for Next Experience, making widgets the stable, mature choice for external portals but not the platform's future direction for internal applications.
Where to Find and Configure It
Navigate to Service Portal > Widgets for the primary widget management interface where you create, edit, and configure widget records including HTML templates, CSS, client controllers, server scripts, and option schemas. Access System Applications > Studio for integrated widget development with syntax highlighting, debugging tools, and version control when working within a scoped application. Use Service Portal > Pages to add widget instances to portal pages and configure their options, layout positioning, and container settings.
Find widgets in action through Service Portal > Portal to see live portal pages with widget instances rendered, and Service Portal > Page Designer for drag-and-drop widget placement and visual page building. Access the underlying sp_widget table directly through System Definition > Tables for advanced filtering, bulk operations, or custom business rule development. Monitor widget performance and errors through System Logs > System Log > All filtering by source sp_widget for server-side script debugging.
Widget behavior differs between scoped and global applications based on the Application field setting on the widget record. Global widgets appear in all portals and can access any table or API, while scoped widgets only appear in portals configured for that application scope and operate under the application's security and API restrictions. Create new widgets through Studio when working in a scoped application to automatically inherit the correct application context, or use Service Portal > Widgets > New and manually set the application scope for global development.
How It Works Step by Step
Widget execution follows a precise server-to-client data flow that begins when a Service Portal page loads or when a client-side event triggers a server refresh. The ServiceNow platform first executes the widget's server script in the context of the portal user's session and application scope, with access to all standard ServiceNow APIs and the special options object containing configuration values from the widget instance. Server script results populate the data object, which gets serialized to JSON and transmitted to the browser where it becomes available to the AngularJS client controller as c.data.
The client controller executes within the AngularJS framework managed by Service Portal, with automatic dependency injection providing access to spUtil, $http, $scope, and other AngularJS services for DOM manipulation, HTTP requests, and inter-widget communication. The HTML template renders with full access to controller scope variables and AngularJS directives, creating the final user interface with two-way data binding between template expressions and controller data. CSS applies automatically through ServiceNow's asset management system, with proper scoping to prevent style conflicts between widgets on the same page.
Enjoying this? Get one deep-dive per week.
Join 1,000+ ServiceNow pros — scripts, GlideRecord patterns, Flow Designer techniques, and career moves. Free.
The Execution Order
- Portal page request arrives at ServiceNow with user session context and portal configuration
- Page layout engine identifies widget instances and their configuration options
- Server script executes for each widget with
optionsobject populated from instance configuration - Server script results populate
dataobject with database queries, calculations, and API responses - ServiceNow serializes
dataobject to JSON and generates complete HTML page with embedded AngularJS application - Browser receives page and AngularJS bootstraps with widget controllers executed in dependency order
- Client controllers access
c.dataand establish event listeners for user interactions - HTML templates render with AngularJS data binding active for dynamic content updates
(function() {
// Server script with options and data objects
var gr = new GlideRecord('incident');
gr.addQuery('state', options.incident_state || '1');
gr.addQuery('assigned_to', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(options.max_records || 10);
gr.query();
data.incidents = [];
while (gr.next()) {
data.incidents.push({
number: gr.getDisplayValue('number'),
short_description: gr.getDisplayValue('short_description'),
priority: gr.getDisplayValue('priority'),
sys_id: gr.getUniqueValue()
});
}
data.total_count = gr.getRowCount();
data.user_name = gs.getUser().getDisplayName();
})();Real-World Scenarios
Building Dynamic Incident Dashboard with Status Filtering
Your IT department needs a portal dashboard showing incident statistics with real-time filtering by state, priority, and assignment group for managers to monitor team workload. The dashboard must update dynamically without page refreshes and display summary counts, recent incidents, and trend charts based on user-selected criteria.
(function() {
var state = options.default_state || '1,2,6';
var priority = options.default_priority || '1,2,3';
var group = options.assignment_group || '';
// Get incident counts by state
var counts = {};
var gr = new GlideAggregate('incident');
if (group) gr.addQuery('assignment_group', group);
gr.addAggregate('COUNT');
gr.groupBy('state');
gr.query();
while (gr.next()) {
counts[gr.getValue('state')] = gr.getAggregate('COUNT');
}
data.incident_counts = counts;
data.filter_options = {
states: state.split(','),
priorities: priority.split(','),
group: group
};
})();Configure widget options schema with fields for default_state, default_priority, and assignment_group to make the widget reusable across different portal pages with different default filters. Watch for GlideAggregate performance with large incident volumes—add database indexes on filtered fields and consider caching results in widget options for high-traffic portals. The client controller needs proper error handling for AJAX requests when users change filter selections, and implement loading indicators to prevent multiple simultaneous server calls.
Creating Self-Service Knowledge Search with Article Rating
Customer portal requires intelligent knowledge article search that displays results based on user query relevance, allows inline article rating without navigation, and tracks search analytics for content optimization. Users must be able to search, preview articles, rate helpfulness, and submit feedback all within a single widget interface.
function($scope, spUtil, $http) {
var c = this;
c.searchQuery = '';
c.articles = c.data.articles || [];
c.searching = false;
c.searchArticles = function() {
if (c.searchQuery.length < 3) return;
c.searching = true;
spUtil.get(c.data.widget_id, {
search_term: c.searchQuery,
category: c.data.category || ''
}).then(function(response) {
c.articles = response.data.articles;
c.searching = false;
});
};
c.rateArticle = function(article, rating) {
$http.post('/api/now/sp/widget/knowledge_rating', {
article_id: article.sys_id,
rating: rating,
widget_id: c.data.widget_id
}).then(function(response) {
article.user_rating = rating;
article.avg_rating = response.data.new_average;
});
};
}Implement server-side search using GlideTextReader for knowledge article indexing and FullTextSearch API for relevance scoring rather than simple GlideRecord queries to match user expectations from modern search interfaces. Configure proper ACLs on knowledge base articles to respect article visibility rules and published states when displaying results to portal users. Watch for search performance with large knowledge bases—implement result pagination and consider using ServiceNow's built-in search analytics to track which searches return zero results for content gap analysis.
Multi-Step Service Request Form with Dynamic Field Dependencies
HR portal needs a complex onboarding request form where field visibility and options change based on previous selections—employee type determines available departments, department selection loads relevant cost centers, and manager approval routing varies by organizational hierarchy. The form must validate dependencies client-side for user experience while maintaining server-side validation for security.
(function() {
// Load initial form structure and dependencies
data.employee_types = [];
var gr = new GlideRecord('hr_employee_type');
gr.addActiveQuery();
gr.orderBy('order');
gr.query();
while (gr.next()) {
data.employee_types.push({
value: gr.getUniqueValue(),
label: gr.getDisplayValue('name'),
departments: gr.getValue('allowed_departments').split(',')
});
}
// Build department lookup with cost center dependencies
data.department_map = {};
gr = new GlideRecord('cmn_department');
gr.addActiveQuery();
gr.query();
while (gr.next()) {
data.department_map[gr.getUniqueValue()] = {
name: gr.getDisplayValue('name'),
cost_centers: gr.getValue('cost_centers').split(','),
requires_approval: gr.getValue('manager_approval') == 'true'
};
}
})();Structure the client controller with watch functions on form fields to trigger dependent field updates and use AngularJS form validation with custom validators that check server-side dependency rules loaded in the initial data object. Implement proper error handling for edge cases where users manipulate form data client-side to bypass dependency rules—always validate dependencies server-side when processing form submission. Consider breaking complex forms into multiple widgets or implementing a wizard pattern with spUtil navigation between steps, as single-widget forms with many dependencies can become difficult to maintain and debug when business rules change.
The Classic Mistake
Using $scope.server.get() or $scope.server.update() in the client controller without proper error handling and loading states.
function($scope) {
// BAD: No loading state, no error handling
$scope.loadData = function() {
$scope.server.get({
action: 'getData',
userId: $scope.data.userId
}).then(function(response) {
$scope.data.records = response.data.records;
$scope.data.total = response.data.total;
});
};
$scope.updateRecord = function(record) {
$scope.server.update().then(function(response) {
$scope.loadData();
});
};
// Immediately call server without checking if data exists
$scope.loadData();
}This fails because users see flickering content, multiple loading indicators, and no feedback when operations fail. ServiceNow's AngularJS digest cycle triggers multiple server calls when the widget re-renders, creating race conditions where newer data gets overwritten by slower responses. The lack of error handling means failed server calls silently break widget functionality, leaving users with stale data and broken interactions.
function($scope) {
$scope.data.loading = false;
$scope.data.error = null;
$scope.loadData = function() {
if ($scope.data.loading) return; // Prevent multiple calls
$scope.data.loading = true;
$scope.data.error = null;
$scope.server.get({
action: 'getData',
userId: $scope.data.userId
}).then(function(response) {
$scope.data.records = response.data.records;
$scope.data.total = response.data.total;
}).catch(function(error) {
$scope.data.error = 'Failed to load data';
console.error('Widget error:', error);
}).finally(function() {
$scope.data.loading = false;
});
};
// Only load if we don't have data
if (!$scope.data.records) {
$scope.loadData();
}
}Always wrap server calls in loading states and error handling. Use loading flags to prevent duplicate requests and provide user feedback for all async operations.
When to Use This vs Alternatives
Widgets are the correct choice when you need custom, interactive user interfaces that integrate with ServiceNow data and require dynamic behavior. They excel at creating dashboard components, custom forms, data visualizations, and user workflows that go beyond what OOB Service Portal pages can deliver.
Choose Widgets When You Need Custom Logic
Use widgets when you need complex client-side interactions, real-time data updates, or custom business logic that can't be achieved with CMS Pages or Knowledge Bases. Standard Service Portal pages are static and lack the server-side scripting capabilities widgets provide. Widgets also win when you need reusable components across multiple portal pages or when building responsive interfaces that adapt to user actions.
Use UI Pages for Platform Integration
Choose UI Pages instead when you need deep platform integration, direct GlideRecord access, or when building interfaces for the main ServiceNow platform UI. UI Pages work better for admin interfaces, system configuration pages, or when you need to leverage platform APIs that aren't exposed to Service Portal. Widgets can't access certain server-side APIs and run in a sandboxed environment.
Combine Widgets with Flow Designer Actions
Use widgets alongside Flow Designer when you need complex backend processing triggered by widget interactions. Create custom Flow actions that widgets can call via REST API or server scripts, allowing you to leverage Flow's integration capabilities while maintaining rich user interfaces. This combination works especially well for approval workflows, external system integrations, and multi-step business processes that require user input.
Platform Interactions & Side Effects
- Widget server scripts bypass Business Rules and ACLs by default - you must explicitly call
gr.setWorkflow(true)to trigger them on GlideRecord operations - All widget instances write to the
sp_instancetable when pages load, creating audit trails but also potential performance overhead on high-traffic portals - Widget CSS is automatically scoped to prevent conflicts, but
!importantdeclarations can still leak to other widgets on the same page - Service Portal session storage persists widget option values across browser sessions, potentially exposing sensitive data in
localStorage - Widget Update Sets capture dependencies automatically, but custom AngularJS directives and external libraries often break during promotion between instances
- Server script execution bypasses the standard
gs.getSession()context and runs with elevated privileges, potentially exposing data beyond user's ACL permissions - Widget caching occurs at the page level in the
cache_entrytable, but server script changes don't invalidate existing cached instances until manual cache flush - Notification scripts triggered from widgets don't inherit the portal user context - they run as the system user and may send emails with incorrect sender information
- Widget dependencies on
sp_dependencytable create circular reference issues when widgets embed other widgets, causing infinite loading loops - Client script errors in widgets break the entire AngularJS scope for the page, preventing other widgets from functioning until page refresh
Debugging and Troubleshooting
Widget failures typically manifest as blank sections on portal pages, infinite loading spinners, or JavaScript console errors about undefined variables. Users see broken layouts or missing content, while admins notice error entries in System Log > System Error with sources pointing to WidgetHelper or SPScriptable. The most common error messages include "Cannot read property of undefined", "$scope.server.get is not a function", and "ReferenceError: variable is not defined" in server scripts.
Debug widget server scripts using gs.log() statements that appear in System Log > All with source "SP" and check client-side issues using browser developer tools console. Enable debug logging by setting glide.service_portal.debug to true for detailed widget execution traces. Server script errors often show stack traces pointing to specific line numbers, while client controller errors require examining the AngularJS scope in browser dev tools.
When widgets fail to load data or display incorrectly, check the sp_log table for Service Portal specific errors and verify widget instance configurations in Service Portal > Pages. Performance issues often stem from inefficient GlideRecord queries in server scripts or missing indexes on queried fields. Use the background script runner to test server script logic independently by copying the script and manually setting option values.
Diagnostic Checklist:
- Check browser console for JavaScript errors and verify all required dependencies are loaded
- Review
System Log > Allfiltered by source "SP" for server script errors - Verify widget options are properly configured and contain expected values using
optionsobject inspection - Test server script logic in background scripts with hard-coded option values to isolate issues
- Clear Service Portal cache via
System Web Services > Service Portal Configurationif changes aren't appearing - Validate HTML template syntax and ensure proper AngularJS directive usage
- Check user permissions and ACL access to tables queried in server scripts
Quick Reference
- Widget server scripts have a 60-second execution timeout and 10MB memory limit per request
- The
dataobject passed from server to client is automatically JSON serialized and has a practical limit of 1MB before performance degrades - Widget CSS executes after Bootstrap and jQuery UI styles, making specificity important for overrides
- Server scripts can access
$sp.getUser()and$sp.getPortalRecord()but not standardgs.getUser()methods - Client controllers run in AngularJS 1.5.11 context and cannot use modern JavaScript features like arrow functions or async/await
- Widget options support JSON objects but lose type information - all values become strings unless explicitly parsed
- Maximum widget nesting depth is 5 levels before ServiceNow prevents further embedding to avoid infinite loops
- Widget instances are cached by page URL and user session, but option changes don't invalidate cache until next browser session
- Server script variables persist across multiple
$scope.server.get()calls within the same user session, potentially causing data leakage between requests - Widget HTML templates support Jinja2-style templating with
${variable}syntax for server-side variable substitution before AngularJS compilation