Service Portal
Guide for building ServiceNow Service Portal experiences using the Fluent API. Service Portal is a portal framework for building user-facing self-service experiences using AngularJS and Bootstrap 3. This guide covers core portal concepts: portals, pages, widgets, and themes.
When to Use
- Building a branded self-service portal for employees, customers, or partners
- Creating custom portal pages with responsive layouts
- Developing interactive widgets with client-server communication
- Customizing portal appearance with themes, headers, and footers
- Setting up navigation menus for portal structure
- Creating shared AngularJS services, directives, and factories for widgets
- Managing external JavaScript and CSS library dependencies for widgets
When NOT to Use
- Platform UI Pages -- use the UI Page skill instead
- Next Experience / UI Builder pages -- different framework entirely
- Backend scripts with no UI component (business rules, script includes, etc.)
- Flow Designer workflows
- Workspace components in Next Experience
Instructions
- Always use dedicated Fluent APIs. Every Service Portal component has a dedicated API (
ServicePortal,SPPage,SPWidget,SPTheme,SPMenu,SPAngularProvider,SPWidgetDependency). Never useRecord()/Now.table()for Service Portal components. - Check OOTB components first. Before creating any widget, theme, or page, check whether an out-of-the-box equivalent already exists. Only create custom components when no suitable OOTB option exists.
- Verify uniqueness.
urlSuffix(portals),pageId(pages), and widgetname/idmust be unique across the instance. Always query the instance to verify before creating. - Bootstrap 3 only. Service Portal uses Bootstrap 3 (not 4 or 5). Use
.panel,.btn-default,.col-xs-*through.col-lg-*, and the 12-column grid. - AngularJS 1.x only. Use controller alias
c(not$scope),ng-repeat,ng-click,ng-model. No Angular 2+ syntax. - Separate files for widgets. Always use
Now.include()for widget scripts, templates, and styles. Never inline large code blocks. - No placeholders. Replace all placeholder values with actual data or queried sys_ids.
- Query only when referencing existing components. Do not query before creating new ones.
- Scoped app restrictions. Fluent apps run in scoped context. Use scoped-safe APIs only (e.g.,
new GlideDateTime()instead ofnowDateTime()). - Run UI diagnostics after install. After a successful install, verify the portal loads without errors at
/<urlSuffix>?id=<homepagePageId>. - Always share the portal URL. After successful install, provide the complete URL:
https://<instance>.service-now.com/<urlSuffix>?id=<homepagePageId>.
Key Concepts
Component Hierarchy
Portal (sp_portal)
+-- Theme (sp_theme)
| +-- Header (sp_header_footer)
| +-- Footer (sp_header_footer)
+-- Main Menu (sp_instance_menu)
+-- Pages (sp_page)
+-- Containers -> Rows -> Columns -> Widget Instances (sp_instance)
+-- Widgets (sp_widget)
+-- Dependencies (sp_dependency)
+-- Angular Providers (sp_angular_provider)
API to Table Mapping
| Component | Fluent API | ServiceNow Table |
|---|---|---|
| Portal | ServicePortal() | sp_portal |
| Page | SPPage() | sp_page |
| Widget | SPWidget() | sp_widget |
| Theme | SPTheme() | sp_theme |
| Menu | SPMenu() | sp_instance_menu |
| Angular Provider | SPAngularProvider() | sp_angular_provider |
| Widget Dependency | SPWidgetDependency() | sp_dependency |
| Header/Footer | Record() | sp_header_footer |
| CSS Include | CssInclude() | sp_css_include |
| JS Include | JsInclude() | sp_js_include |
File Organization
fluent/
+-- service-portal/
| +-- portal.now.ts
+-- sp-page/
| +-- home/
| | +-- home-page.now.ts
| +-- login/
| +-- login-page.now.ts
+-- sp-widget/
| +-- my_widget/
| +-- widget.now.ts
| +-- server_script.js
| +-- client_script.js
| +-- template.html
| +-- styles.css
+-- sp-theme/
| +-- theme.now.ts
+-- sp-instance-menu/
| +-- menu.now.ts
+-- sp-angular-provider/
| +-- provider.now.ts
+-- sp-dependency/
| +-- dependency.now.ts
+-- sp-header-footer/
+-- header.now.ts
Important Technologies
Service Portal uses Bootstrap 3 (not 4/5) and AngularJS 1.x (not Angular 2+):
- Bootstrap 3: 12-column grid,
.panel,.btn-default,.col-xs-*through.col-lg-* - AngularJS: controller alias
c(not$scope),ng-repeat,ng-click,ng-model
AngularJS Binding Reference
| Pattern | Usage |
|---|---|
| Two-way data binding | ng-model="c.data.fieldName" |
| Display only | ng-bind="c.data.value" or {{c.data.value}} |
| Remove from DOM | ng-if="c.data.showSection" |
| Hide/show (keep in DOM) | ng-show="c.loading" |
| List iteration | ng-repeat="item in c.data.rows track by item.sys_id" |
| Click handler | ng-click="c.methodName()" |
| Dynamic class | ng-class="{'sp-active': c.selected === item.sys_id}" |
| Disable button | ng-disabled="c.submitting" |
| Input validation state | ng-class="{'has-error': c.errors.fieldName}" |
Widget Script Communication
Server script context:
data-- object passed to clientinput-- client input fromc.server.get()/c.server.update()options-- widget option values
Client script context:
c.data-- data from serverc.options-- widget optionsc.server.get(input)-- call server (GET)c.server.update()-- call server (POST, sendsc.dataasinput)c.server.refresh()-- reload widget