Service Portal

Add a Typeahead Search to a Service Portal Widget

Typeahead search fields let users find records quickly without scrolling through long dropdowns or memorizing exact names. This guide walks you through building one that queries any ServiceNow table and handles the performance considerations that trip up most implementations.

Why typeahead beats reference fields in portals

Portal users expect search-as-you-type behavior from modern web applications, but ServiceNow reference fields show system UI components that look out of place and confuse end users. Portal developers end up building custom search fields that query tables directly, but most implementations fire a server call on every keystroke — with predictably bad performance. The people maintaining these widgets inherit slow-loading portals and frustrated users who complain that 'typing anything takes forever.'

Building typeahead with Angular and spUtil

A Service Portal typeahead combines Angular's typeahead directive on the client with spUtil.get() calls to a server script that runs GlideRecord queries. The client script captures keystrokes and calls the server, the server script queries the target table and returns matching records, and Angular displays the results in a dropdown. The key is debouncing the input to avoid hammering the server and using GlideRecord.setLimit() to cap result sets. Most developers start with a basic query and add filtering, sorting, and result formatting over time.

Production-quality improvements

Once basic search works, the enhancements that matter are: adding minimum character requirements before searching starts, implementing proper error handling for failed server calls, and caching results to avoid repeat queries for the same input. Consider adding keyboard navigation for accessibility, highlighting search terms in results, and configuring which fields get searched beyond just the display value. Well-built typeaheads also handle edge cases like special characters in search terms and provide visual feedback when searches are running.

Before you start

  • widget_editor role or admin role
  • Target table readable by portal users or public role
Sourdough
Chrome Extension

Sourdough: ServiceNow Monitoring and Analytics

A Chrome extension for ServiceNow Admins and Developers with essential tools, analytics, graphs and monitoring features.

Instance HealthGraphs & ChartsAPI HealthDeveloper ToolsQuick SearchInstance Switcher
Add to Chrome

Free to install. Pro $5/month after a 14-day no-card trial.
Pro requires the ServiceNow admin role. Upgrade inside the extension.

Overview
Tasks
CMDB
API
Metrics
Monitor
Internals
Instance:sourdoughdev·Version:Yokohama
Instance StateONLINE
System StatusFully Operational
Session Timeout90 minutes
Logged-In Sessions2 (20 active)
Build Nameyokohama-12-18-2024_p1
IP Address10.159.128.43
Instance HealthHealth Score: 90%
🔥 5dSourdough (Chrome Plugin)Dark Mode

Step by step

1

Create the widget structure

Navigate to Service Portal > Widgets and create a new widget. Set the Name and ID to something descriptive like 'Typeahead Search'. In the HTML template, add an input field with Angular's typeahead directive: `<input type="text" ng-model="c.selectedItem" uib-typeahead="item as item.display_value for item in c.searchItems($viewValue)" class="form-control">`. The searchItems function will handle the server calls.

TIP

Use 'uib-typeahead' not 'typeahead' — the older directive has known issues in Service Portal.

2

Build the client script debounce

In the Client Script, create the searchItems function that debounces user input: `c.searchItems = function(searchTerm) { if (!searchTerm || searchTerm.length < 3) return []; clearTimeout(c.searchTimeout); return new Promise(function(resolve) { c.searchTimeout = setTimeout(function() { spUtil.get(c, {action: 'search', term: searchTerm}).then(function(response) { resolve(response.data.results); }); }, 300); }); };`. This waits 300ms after the user stops typing before calling the server.

TIP

The 3-character minimum prevents expensive queries on single letters that return too many results.

3

Write the server-side query

In the Server Script, handle the search action with a GlideRecord query: `if (input && input.action === 'search') { var gr = new GlideRecord('your_table_name'); gr.addQuery('name', 'CONTAINS', input.term); gr.setLimit(10); gr.orderBy('name'); gr.query(); var results = []; while (gr.next()) { results.push({sys_id: gr.getUniqueValue(), display_value: gr.getDisplayValue('name')}); } data.results = results; }`. Replace 'your_table_name' and 'name' with your target table and search field.

TIP

Always use setLimit() — without it, searches for common terms can return thousands of records and kill performance.

4

Handle selection and validation

Add selection handling to the client script: `c.onSelect = function($item) { c.selectedRecord = $item; c.data.selected_sys_id = $item.sys_id; };` and attach it to the input with `typeahead-on-select="c.onSelect($item)"`. For validation, add `typeahead-no-results="c.noResults = true"` to show feedback when no matches are found.

TIP

Store both the display value and sys_id — you'll need the sys_id for any server-side processing of the selected record.

5

Add error handling and loading states

Wrap the spUtil.get() call in error handling: `spUtil.get(c, {action: 'search', term: searchTerm}).then(function(response) { resolve(response.data.results || []); }).catch(function(error) { console.error('Search failed:', error); resolve([]); });`. Add a loading indicator by setting `c.searching = true` before the call and `c.searching = false` in both the success and error handlers.

TIP

Users expect visual feedback when searches are running — add `ng-show="c.searching"` to a spinner or loading text.

6

Configure widget options

Add option schema to make the widget reusable: `[{"name": "table", "label": "Table", "type": "string"}, {"name": "search_field", "label": "Search Field", "type": "string", "default_value": "name"}, {"name": "max_results", "label": "Max Results", "type": "integer", "default_value": 10}]`. Update the server script to use `options.table` instead of the hardcoded table name and `options.search_field` for the query field.

TIP

Make the minimum character count an option too — different use cases need different thresholds.

Best practices

  • Always use setLimit() in your GlideRecord query — searches without limits can return thousands of records and make the portal unusable.

  • Debounce typeahead calls with at least 250ms delay — firing server requests on every keystroke will overwhelm slower instances.

  • Set a minimum character requirement before searching starts — queries on single characters are expensive and rarely useful.

  • Handle the case where spUtil.get() fails by returning an empty array — network issues shouldn't break the entire widget.

  • Clear previous timeouts before setting new ones in the debounce function — without this, fast typers will trigger multiple overlapping searches.

Test Your Knowledge

Quick 3-question quiz — see how your ServiceNow skills stack up.

Question 1 of 3Performance

A list view on a table with millions of records is slow. Best fix?

Select an answer to continue