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: ServiceNow Monitoring and Analytics
A Chrome extension for ServiceNow Admins and Developers with essential tools, analytics, graphs and monitoring features.
Free to install. Pro $5/month after a 14-day no-card trial.
Pro requires the ServiceNow admin role. Upgrade inside the extension.
Step by step
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.
Use 'uib-typeahead' not 'typeahead' — the older directive has known issues in Service Portal.
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.
The 3-character minimum prevents expensive queries on single letters that return too many results.
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.
Always use setLimit() — without it, searches for common terms can return thousands of records and kill performance.
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.
Store both the display value and sys_id — you'll need the sys_id for any server-side processing of the selected record.
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.
Users expect visual feedback when searches are running — add `ng-show="c.searching"` to a spinner or loading text.
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.
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.
A list view on a table with millions of records is slow. Best fix?
Select an answer to continue