Servicenow connector for Simpplr Enterprise Search

Overview

The Servicenow connector allows Simpplr Enterprise Search to index Servicenow Storage content, making it easily discoverable and searchable directly within Simpplr.

With this connector, you can (use cases):

  • Bring  Servicenow content into Simpplr Enterprise Search so users can find files alongside intranet content in one place.
  • Respect Servicenow access permissions so users only see files they already have access to in Servicenow.
  • Use advanced features like autocomplete, hybrid ranking, and Smart Answers on top of Servicenow content.

Indexed content from Simpplr Enterprise Search is available in:

  • In main search listing
  • Smart answers

Capabilities at a glance

Content typesFolders, Files, pdf, ppt, doc, word, excel, csv, text etc.
Metadataid, sys_id, article_id, number, display_number, short_description, text, autocomplete, topic, article_type, workflow_state, active, latest, published, valid_to, author.value, kb_knowledge_base.value, kb_category.value, sys_created_by, sys_created_on, sys_updated_by, sys_updated_on, sys_view_count, use_count, helpful_count, connector_type, _timestamp, _document_hash
PermissionsUser Criteria Permissions
IndexingInitial full crawl when the connector is created, followed by a weekly full crawl. Incremental updates run every 4 hours, and ACL (permission) sync runs every hour.
Multiple instances supportMultiple Servicenow connections can be configured in the Simpplr environment.
Search features

Pre-ingestion filters

  • Audience filters - Admins can include/exclude documents from indexing based on the Audiences.

Filtering follows an after-scan approach, meaning the entire dataset is scanned first and then filtered based on the specified fields.

  • Keyword search
  • Hybrid / semantic ranking
  • Autocomplete suggestions
  • Filters for source, file type, owner, created date
  • Servicenow content can be used in Smart Answers

Objects and content supported

  1. Objects - List the object types that are indexed, for example:
  • Files
  1. Metadata - For each indexed item, Servicenow captures:
  • Name
  • URL / link
  • Owner details
  • Update by details
  • Created time and Updated Time
  • Parent Details
  • Object Type
  • File extension and size
  • Mime Type
  • Permissions (users and groups level access)
  1. Permissions model - Permissions are read from Servicenow and enforced in Simpplr Enterprise Search: 
    1. How user and group permissions are synchronized
  • Servicenow user and group memberships are fetched and stored in the ACL index.
  • When a user is added to or removed from a Servicenow group, the ACL index is updated the next time the ACL sync runs (by default, every hour).
  1. How public or link-shared content is handled
  • Content that is only available via anonymous or public shared links is not indexed in the current version.
  1. What happens when access is removed for a specific document
  • When a user loses access to a file or folder in Servicenow, the updated permissions are applied during the next ACL sync.
  • The file will no longer appear in that user’s Simpplr search results after the ACL sync completes.

Versions and editions supported

  • Supports enabled Servicenow service.

Prerequisites

Before you begin, ensure the following:

  1. Source system Access
  • Access to Microsoft Entra user account
  1. Application / service account permissions
  • Sufficient permissions to register an application with your Microsoft Entra tenant, and assign to the application a role in your Azure subscription. To complete these tasks, you'll need the Application.ReadWrite.All permission.
  • Ability to grant the admin consent to the application from the Admin console (If you are not an admin, you need to request the Admin to grant consent via their Azure Portal).

Authentication and security

  1. Authentication mechanism
    Describe how  Simpplr Enterprise Search connects to Servicenow:
  • Auth type: Basic Authentication (Client credentials)
  • Scopes or permissions required:
    • N/A
  1. Data security
    1. Data storage and residency: Indexed content and ACLs from Servicenow are stored within your Simpplr Enterprise Search environment, in the same region as your Simpplr tenant.
    2. Encryption in transit: Server-side encryption with Amazon S3 managed keys (SSE-S3), TLS encryption in Kafka.
    3. Encryption at rest: SSL (TLS 1.2 or higher), Auth: OAuth 2.0 client-credentials.
    4. Permission enforcement: Servicenow access controls (users and groups) are stored in the ACL index and applied at query time. Search results are always filtered by the signed-in user’s identity and Servicenow group memberships.

Setup and configuration

Step 1 - Prepare Servicenow source.

  1. We will be requiring your Service now 

 1.1 Instance URL 
1.2 Servicenow Admin Username
1.3 Servicenow Admin Password

  1. For ACL we would need few more steps : 
    Create the Scripted REST API Service for User Criteria API
  • Navigate to System Web Services > Scripted Web Services > Scripted REST APIs
  • Click New
  • Fill in the following fields:
    • Name: Get User Criteria
    • API ID: Will auto-populate as get_user_criteria_api
    • Active: Checked
  • Click Submit
  • The system will generate a Base API path like /api/x_your_scope/get_user_criteria_api
  1. Create the Scripted REST API  Script for User Criteria API 
    • Open your newly created Scripted REST API record
    • In the Resources related list, click New
    • Fill in the resource details:
      • Name: Get User Criteria
      • HTTP method: GET
      • Relative path: /user_criteria/{user_id}
      • Active: Checked
    • In the Script field, paste the working script provided below
    • Under Security section:
      • Check Requires authentication
      • Check Requires ACL authorization
      • Set ACLs to the Scripted Rest External Default
    • Click Submit

   

  1. Paste the below script : 
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    // Log request entry
    gs.info("User Criteria API: Received request");

    try {
        // FIX: Extract user ID from path parameters instead of query parameters
        var pathParams = request.pathParams;
        gs.info("User Criteria API: Path parameters: " + JSON.stringify(pathParams));
        
        // Extract user_id from path parameter and convert to string
        var userID = pathParams.user_id ? String(pathParams.user_id) : '';
        
        // Log incoming user parameter
        gs.info("User Criteria API: Received user ID: " + userID);

        if (!userID || userID == 'undefined' || userID == '') {
            gs.error("User Criteria API: Missing user_id parameter in request path.");
            response.setStatus(400);
            response.setBody({
                error: "Bad Request",
                message: "User ID is required in the path. Use format: /user_criteria/{user_id}"
            });
            return;
        }

        // Track user criteria loading
        gs.info("User Criteria API: Invoking UserCriteriaLoader for user " + userID);

        var allCriterias = new sn_uc.UserCriteriaLoader().getAllUserCriteria(userID);
        gs.info("User Criteria API: Criteria sys_ids found: " + JSON.stringify(allCriterias));
        gs.info("User Criteria API: Number of criteria matched: " + allCriterias.length);

        var criteriaDetails = [];
        for (var i = 0; i < allCriterias.length; i++) {
            gs.debug("User Criteria API: Processing criteria sys_id: " + allCriterias[i]);
            var ucGR = new GlideRecord('user_criteria');
            if (ucGR.get(allCriterias[i])) {
                // Convert values to plain strings to avoid serialization issues
                criteriaDetails.push({
                    sys_id: String(ucGR.getValue('sys_id')),
                    name: String(ucGR.getValue('name')),
                    active: String(ucGR.getValue('active')),
                    match_all: String(ucGR.getValue('match_all')),
                    advanced: String(ucGR.getValue('advanced'))
                });
                gs.debug("User Criteria API: Added details for criteria: " + ucGR.getValue('name'));
            } else {
                gs.error("User Criteria API: No record found for criteria sys_id: " + allCriterias[i]);
            }
        }

        // Log response composition
        gs.info("User Criteria API: Returning " + criteriaDetails.length + " criteria details");
        
        // Return empty object if no criteria found
        if (criteriaDetails.length === 0) {
            response.setStatus(200);
            response.setBody({});
        } else {
            var responseBody = {
                user_id: String(userID),
                total_criteria: parseInt(criteriaDetails.length, 10),
                user_criteria: criteriaDetails
            };
            
            response.setStatus(200);
            response.setBody(responseBody);
        }

    } catch (error) {
        gs.error("User Criteria API: Error encountered: " + error.message);
        gs.error("User Criteria API: Error stack: " + error.stack);
        response.setStatus(500);
        response.setBody({
            error: "Internal Server Error",
            message: String(error.message)
        });
    }
})(request, response);​
  1. Create the Scripted REST API service for document user criteria API

    • Navigate to System Web Services > Scripted Web Services > Scripted REST APIs
    • Click New
    • Fill in the following fields:
      • Name: Knowledge Document Permissions
      • API ID: Will auto-populate as knowledge_document_permissions
      • Active: Checked
    • Click Submit
    • The system will generate a Base API path like /api/x_your_scope/knowledge_document_permissions 
            
  2. Create the Scripted REST API  Script for Knowledge Permission API  API 
    • Open your newly created Scripted REST API record
    • In the Resources related list, click New
    • Fill in the resource details:
      • Name: Knowledge permissions
      • HTTP method: GET
      • Relative path: /users/{document_id}
      • Active: Checked
    • In the Script field, paste the working script provided below
    • Under Security section:
      • Check Requires authentication
      • Check Requires ACL authorization
      • Set ACLs to the Scripted Rest External Default
    • Click Submit
  3. Paste the Below Script : 
(function execute(request, response) {
    
    // Get document ID from URL parameters
    var documentId = request.pathParams.document_id;
    
    // Validate input
    if (!documentId) {
        response.setStatus(400);
        response.setBody({
            error: 'Document ID is required',
            message: 'Please provide a valid document sys_id in the URL path'
        });
        return;
    }
    
    try {
        // Get the knowledge article
        var kbArticle = new GlideRecord('kb_knowledge');
        if (!kbArticle.get(documentId)) {
            response.setStatus(404);
            response.setBody({
                error: 'Document not found',
                message: 'Knowledge article with ID ' + documentId + ' does not exist'
            });
            return;
        }
        
        var result = {
            document_id: documentId,
            document_number: kbArticle.getValue('number'),
            document_title: kbArticle.getValue('short_description'),
            knowledge_base: kbArticle.getDisplayValue('kb_knowledge_base'),
            user_criteria: {
                article_level: {
                    can_read: [],
                    cannot_read: []
                },
                knowledge_base_level: {
                    can_read: [],
                    cannot_read: []
                }
            },
            access_evaluation: {}
        };
        
        // Get article-level user criteria
        var articleCanRead = kbArticle.getValue('can_read_user_criteria');
        var articleCannotRead = kbArticle.getValue('cannot_read_user_criteria');

        gs.info('=== USER CRITERIA ===');
        gs.info(articleCanRead);
        
        if (articleCanRead) {
            result.user_criteria.article_level.can_read = getUserCriteriaDetails(articleCanRead);
        }
        
        if (articleCannotRead) {
            result.user_criteria.article_level.cannot_read = getUserCriteriaDetails(articleCannotRead);
        }
        
        // Get knowledge base level user criteria
        if (kbArticle.knowledge_base) {
            var kbId = kbArticle.getValue('kb_knowledge_base');
            
            // Get KB can read criteria
            var kbCanRead = new GlideRecord('kb_uc_can_read_mtom');
            kbCanRead.addQuery('kb_knowledge_base', kbId);
            kbCanRead.query();
            
            while (kbCanRead.next()) {
                var criteria = getUserCriteriaDetails(kbCanRead.getValue('user_criteria'));
                result.user_criteria.knowledge_base_level.can_read = 
                    result.user_criteria.knowledge_base_level.can_read.concat(criteria);
            }
            
            // Get KB cannot read criteria  
            var kbCannotRead = new GlideRecord('kb_uc_cannot_read_mtom');
            kbCannotRead.addQuery('kb_knowledge_base', kbId);
            kbCannotRead.query();
            
            while (kbCannotRead.next()) {
                var criteria = getUserCriteriaDetails(kbCannotRead.getValue('user_criteria'));
                result.user_criteria.knowledge_base_level.cannot_read = 
                    result.user_criteria.knowledge_base_level.cannot_read.concat(criteria);
            }
        }
        
        // Optional: Evaluate access for current user
        var currentUserId = gs.getUserID();
        if (currentUserId) {
            result.access_evaluation.current_user = {
                user_id: currentUserId,
                user_name: gs.getUserName(),
                has_access: evaluateUserAccess(currentUserId, result.user_criteria)
            };
        }
        
        response.setStatus(200);
        response.setBody(result);
        
    } catch (error) {
        gs.error('Error in Knowledge User Criteria API: ' + error.message);
        response.setStatus(500);
        response.setBody({
            error: 'Internal server error',
            message: 'An error occurred while processing the request'
        });
    }
    
    // Helper function to get user criteria details
    function getUserCriteriaDetails(criteriaIds) {
        if (!criteriaIds) return [];
        
        var criteriaArray = criteriaIds.split(',');
        var details = [];
        
        for (var i = 0; i < criteriaArray.length; i++) {
            var ucRecord = new GlideRecord('user_criteria');
            if (ucRecord.get(criteriaArray[i])) {
                gs.info('=== COMPLETE ARTICLE DETAILS ===');
                var fields = ucRecord.getFields();
                for (var j = 0; j < fields.size(); j++) {
                    var fieldName = fields.get(j).getName();
                    var fieldValue = ucRecord.getValue(fieldName);
                    var displayValue = ucRecord.getDisplayValue(fieldName);
                    
                    gs.info(fieldName + ': ' + fieldValue + 
                          (displayValue !== fieldValue ? ' [Display: ' + displayValue + ']' : ''));
                }
                
                details.push({
                    sys_id: ucRecord.getValue('sys_id'),
                    name: ucRecord.getValue('name'),
                    description: ucRecord.getValue('description'),
                    users: getUsersFromCriteria(ucRecord.getValue('user')),
                    groups: getGroupsFromCriteria(ucRecord.getValue('group')),
                    roles: getRolesFromCriteria(ucRecord.getValue('role')),
                    departments: getDepartmentsFromCriteria(ucRecord.getValue('department')),
                    companies: getCompaniesFromCriteria(ucRecord.getValue('company')),
                    locations: getLocationsFromCriteria(ucRecord.getValue('location')),
                    advanced_script: ucRecord.getValue('script') ? true : false,
                    match_all: ucRecord.getValue('match_all')
                });
            }
        }
        
        return details;
    }
    
    // Helper function to get users from criteria
    function getUsersFromCriteria(userIds) {
        if (!userIds) return [];
        
        var userArray = userIds.split(',');
        var users = [];
        
        for (var i = 0; i < userArray.length; i++) {
            var userRecord = new GlideRecord('sys_user');
            if (userRecord.get(userArray[i])) {
                users.push({
                    sys_id: userRecord.getValue('sys_id'),
                    user_name: userRecord.getValue('user_name'),
                    name: userRecord.getValue('name'),
                    email: userRecord.getValue('email')
                });
            }
        }
        
        return users;
    }
    
    // Helper function to get groups from criteria
    function getGroupsFromCriteria(groupIds) {
        if (!groupIds) return [];
        
        var groupArray = groupIds.split(',');
        var groups = [];
        
        for (var i = 0; i < groupArray.length; i++) {
            var groupRecord = new GlideRecord('sys_user_group');
            if (groupRecord.get(groupArray[i])) {
                groups.push({
                    sys_id: groupRecord.getValue('sys_id'),
                    name: groupRecord.getValue('name'),
                    description: groupRecord.getValue('description')
                });
            }
        }
        
        return groups;
    }
    
    // Helper function to get roles from criteria
    function getRolesFromCriteria(roleIds) {
        if (!roleIds) return [];
        
        var roleArray = roleIds.split(',');
        var roles = [];
        
        for (var i = 0; i < roleArray.length; i++) {
            var roleRecord = new GlideRecord('sys_user_role');
            if (roleRecord.get(roleArray[i])) {
                roles.push({
                    sys_id: roleRecord.getValue('sys_id'),
                    name: roleRecord.getValue('name'),
                    description: roleRecord.getValue('description')
                });
            }
        }
        
        return roles;
    }
    
    // Helper function to get departments from criteria
    function getDepartmentsFromCriteria(departmentIds) {
        if (!departmentIds) return [];
        
        var deptArray = departmentIds.split(',');
        var departments = [];
        
        for (var i = 0; i < deptArray.length; i++) {
            var deptRecord = new GlideRecord('cmn_department');
            if (deptRecord.get(deptArray[i])) {
                departments.push({
                    sys_id: deptRecord.getValue('sys_id'),
                    name: deptRecord.getValue('name'),
                    description: deptRecord.getValue('description'),
                    dept_head: deptRecord.getDisplayValue('dept_head'),
                    company: deptRecord.getDisplayValue('company')
                });
            }
        }
        
        return departments;
    }
    
    // Helper function to get companies from criteria
    function getCompaniesFromCriteria(companyIds) {
        if (!companyIds) return [];
        
        var companyArray = companyIds.split(',');
        var companies = [];
        
        for (var i = 0; i < companyArray.length; i++) {
            var companyRecord = new GlideRecord('core_company');
            if (companyRecord.get(companyArray[i])) {
                companies.push({
                    sys_id: companyRecord.getValue('sys_id'),
                    name: companyRecord.getValue('name'),
                    phone: companyRecord.getValue('phone'),
                    fax_phone: companyRecord.getValue('fax_phone')
                });
            }
        }
        
        return companies;
    }
    
    // Helper function to get locations from criteria
    function getLocationsFromCriteria(locationIds) {
        if (!locationIds) return [];
        
        var locationArray = locationIds.split(',');
        var locations = [];
        
        for (var i = 0; i < locationArray.length; i++) {
            var locationRecord = new GlideRecord('cmn_location');
            if (locationRecord.get(locationArray[i])) {
                locations.push({
                    sys_id: locationRecord.getValue('sys_id'),
                    name: locationRecord.getValue('name'),
                    city: locationRecord.getValue('city'),
                    state: locationRecord.getValue('state'),
                    country: locationRecord.getValue('country'),
                    street: locationRecord.getValue('street'),
                    full_name: locationRecord.getValue('full_name')
                });
            }
        }
        
        return locations;
    }
    
    // Helper function to evaluate user access using UserCriteriaLoader
    function evaluateUserAccess(userId, userCriteria) {
        try {
            // Use ServiceNow's UserCriteriaLoader API for evaluation
            var allUserCriteria = [];
            
            // Collect all user criteria IDs
            if (userCriteria.article_level.can_read) {
                userCriteria.article_level.can_read.forEach(function(criteria) {
                    allUserCriteria.push(criteria.sys_id);
                });
            }
            
            if (userCriteria.knowledge_base_level.can_read) {
                userCriteria.knowledge_base_level.can_read.forEach(function(criteria) {
                    allUserCriteria.push(criteria.sys_id);
                });
            }
            
            // Check if user matches any criteria using UserCriteriaLoader
            if (allUserCriteria.length > 0) {
                var loader = new sn_uc.UserCriteriaLoader();
                return loader.userMatches(userId, allUserCriteria);
            }
            
            return false;
            
        } catch (error) {
            gs.error('Error evaluating user access: ' + error.message);
            return null;
        }
    }
    
})(request, response);


Step 2 - Create the connector in Enterprise Application

  1. In Simpplr, go to: Manage Features → Enterprise Search → Add Source.

  2. Search and Select “ Microsoft Servicenow”.

  1. Enter basic information:
    • Connection Name: (Connector Name for this instance)
  2. Provide authentication details and select the certificate toggle in authentication method  (Copied from the client Application):
    • Servicenow Instance URL
    • Servicenow Admin Username
    • Servicenow Admin Password

  • Configure Common Filters rules (Inclusion only):
    • Knowledge Base
  • Configure Audience based filtering.
    • Include audiences
    • Exclude audiences

Step 4 - Configure sync schedule.

  • Default schedule: Full crawl at first setup and once in a week, incremental sync every 4 hours, ACL runs every hour
  • Configuration options:
    • No option to configure the sync schedule, however sync can be paused and resumed manually

Step 5 - Monitor the sync

  1. Monitor the initial full sync status (starts automatically) in the connector dashboard.

Crawling and sync behavior

How the connector works over time:

  1.  Initial full crawl
  • All the content present in the Servicenow Storage account is indexed during the first run
  • How long it may take: Depends on the size of the content.
  1. Incremental updates
  • Mechanism: Based on Timestamp of previous sync.
  • What changes trigger reindexing:
    • New items created
    • Existing items updated
    • Permissions changed
    • Items moved or renamed
    • Items deleted or archived
  1. Deletion and permission changes
  • Deleted items are removed from index at next sync.
  • Permission changes are updated at the next sync cycle.
  1. Expected latency
  • With the default schedule (incremental sync every 4 hours and ACL sync every hour), changes made to Servicenow content are generally reflected in Simpplr search results within 4 hours of the update, and the permission lag in the system can be up-to 4hours. (as the incremental Sync every 4 hours). On top of that, there can be certain cases, where the permission sync can take up-to 7days (When the full sync is run), subject to content volume and system load.

Field mapping and search experience

  1. Default field mapping

Source field Servicenow → Index field Simpplr

  1.  Search experience - how content from this connector appears in search:
  • Result layout: (Icon, Connector name, Folder name, title (name) as link, body(excerpt), Created Date, File Type icon, file type)

  • Available filters and facets:
    • Sources = Servicenow
    • File type
    • Owner
    • Created Date
  • Participation in advanced features:
    • Smart Answers / Q&A: Yes
    • Autocomplete: Yes
    • Recommendations / “Suggested for you”: N/A
    • Trending / popular results: N/A
    • Semantic / hybrid ranking: Yes
  1. Limits and known limitations
Maximum file size indexedFiles bigger than 10 MB won’t be extracted.
Unsupported file types

Compressed files are not supported, e.g., an archive file containing a set of PDFs

(The file content is not searchable, however, the users can still search via file title.)

Rate limitsN/A
Preview limitationsNo preview available for excel, or media files.
Permission edge casePermission changes are not synced unless ACL sync is run.
Other known limitationsThe text from the PDF is extracted. However,  If text is within an image, it is not extracted.

Monitoring and troubleshooting

  1. Connector health and monitoring - Describe where admins can see status information:
  • Enterprise Search → Connector name
  • Available metrics:
    • Last sync status (Success / Warning / Failed)
    • Last sync time
    • Next scheduled sync
    • Sync Type
    • Total items indexed count

  1. Common issues and resolutions. Example pattern:
    1. Issue: Authentication failed, Failed to fetch the access token (invalid credentials or missing scopes)
      Possible causes:
      • Incorrect client ID or secret
      • App not granted the required permissions
      • App not granted with the Admin consent
    2. Resolution:
      • Verify and re-enter credentials
      • Confirm required permissions are granted
      • Confirm if the App is granted with the Admin consent.
  2. When to contact support. 
    • Authentication error persists even after trying the above-mentioned resolutions
    • Sync is stuck in the Pending state,
    • Sync is in progress but no documents are getting ingested.
    • Sync failure with cancelled error (when not cancelled manually)
    • Incomplete or Partial sync.
  3. When contacting Support, include:
  • Connector name and instance ID (if available)
  • Organization URL
  • Approximate time and date of the issue
  • Error messages or screenshots
  • Steps you already tried

Frequently asked questions (FAQ)

Q1. Can I connect multiple Servicenow tenants or domains?
A. Multiple Servicenow connections can be configured in the Simpplr environment. But functionality to connect multiple tenants or domains in a single connector is not implemented yet.

Q2. How often does Servicenow sync data?
A. The connector runs a full crawl on first setup and then once per week. Incremental sync runs every 4 hours and ACL (permission) sync runs every hour by default.

Q3. Are comments, revisions, or version history indexed?
A. Comments and individual versions are not indexed as separate items. The connector indexes the latest file metadata, including the last updated time and updated-by user.

Q4. Does the connector index content from external guests or shared links?
A. 

Q5. What happens when a user loses access to an item in Servicenow Storage ?
A. The updated access permissions will be indexed during the next sync.

Note:

Though the access control syncs run every hour, the permission lag in the system can be up-to 4hours. (As the incremental sync every 4 hours). On top of that, there can be certain cases, where the permission sync can take up-to 7days (When the full sync is run).

Q6. Can I exclude certain sites/teams/folders from being indexed?
A. Documents can be included and excluded based on the Sites and IDs. Documents can also be excluded based on file extension, size, and age. Additionally, documents can be included or excluded based on audiences.

Q7. How are deletions handled?
A. Objects deleted from the source are permanently deleted from the index.

Q8. Are image files searchable ?
A. No, images are not searchable.

Was this article helpful?
0 out of 0 found this helpful
Have more questions? Submit a request
Note: Some features may not be avalable in your instance due to various packaging and pricing. To learn what features are available to your org and bundling with the Simpplr One packaging, contact your CSM or Account Manager.

Comments

0 comments

Please sign in to leave a comment.

Articles in this section