Background
While working through a subscription or licensing challenge in ServiceNow HRSD, I searched extensively for solutions or shared experiences from others in the ServiceNow community. Surprisingly, I found no posts or discussions addressing this specific issue, particularly around the automatic creation of HR Profiles for users who never interact with HR services. This gap prompted me to document the approach taken in one organisation, in the hope that it can offer a practical reference or spark ideas for others facing similar challenges.
Purpose of the Optimisation
This optimisation was not created to bypass ServiceNow’s HRSD subscription model. It was designed to correct how HR Profiles are generated and to encourage organisations to review their actual usage patterns. The goal is to prevent unnecessary data collection and avoid the costs that come with maintaining profiles that are never used.
In the organisation I worked with, the initial analysis showed that contractors and temporary employees do not actively use ServiceNow. They may access it occasionally, but they are not part of the onboarding or offboarding processes handled within HRSD.
Both contractors and employees exist in systems like Active Directory, SAP, and other HR databases, but that does not automatically mean they need to interact with ServiceNow. For many of these users, ServiceNow is only relevant for categorisation or as part of lookups by HR or back-end users.
However, because of how integrations work, any user record synced from external systems can trigger automatic HR Profile creation in ServiceNow. This means that even users who never log in or use HR services can still generate HR Profiles, resulting in significant, unnecessary costs over time.
Key Challenges in System Integrations
Reducing unnecessary subscription costs
HR Profile creation in ServiceNow is often driven by automated integrations with systems such as SAP, Active Directory, and other internal databases. These integrations can result in the blanket creation of HR Profiles for all users, regardless of whether they actively engage with HR services. Since an HR Profile in ServiceNow defines a user license, this can lead to significant subscription costs for users who never raise a case or interact with HR functionality.
Differentiating profile creation
To address this, a more selective approach was implemented. HR Profiles continue to be automatically created for employees, as they are part of the onboarding and offboarding lifecycle. For external users such as contractors or consultants (where the employment group is marked as ‘external’), HR Profiles are only generated when they raise an HR case or when a case is opened on their behalf (for example, as the “Opened for” or “Subject Person”).
Balancing cost and data retention
External user data is still included in the integration process. Their records are held in a staging table (HR Profile Staging) during the daily run to retain key information such as start dates and employment status. This approach helps manage licensing costs while preserving essential data for categorisation, reporting, and search purposes.

The Solution
Management of HR Profiles through System Integrations
In the organisation where this use case was applied, the daily integration process is driven by SAP. However, similar challenges can arise from other integrations such as Active Directory or internal HR databases. The integration updates a staging table (sn_hr_core_profile_staging) instead of directly modifying the main HR Profile table (sn_hr_core_profile). The SAP run typically starts at 1 AM and lasts 1 to 2 hours.
At the end of a standard import, a Transform Map onComplete event script is expected to process data from the staging table, moving records into the HR Profile table. This includes adding new employee records and managing external users based on HR case activity. However, due to the synchronous nature of the SAP connection, the onComplete script triggers for every row, making it difficult to manage new records in staging and remove contractors who do not have any associated HR cases.
To improve this process, a Scheduled Job named Daily Run to Process HR Profiles was introduced. This job runs every 12 hours, starting at 4 AM, to accommodate longer integration durations. It processes records from the staging table and ensures that only relevant data is maintained in the HR Profile table.
Auto-Creation of HR Profiles
HR Profiles are automatically created under certain conditions when ServiceNow detects a user in the sys_user table but not in the sn_hr_core_profile table. The default ServiceNow logic creates these profiles even if the user has no HR case activity. Adjustments were made to this behaviour to align with the organisation’s integration process and reduce unnecessary license consumption.
Details about the related records, including business rules and scripts that influence profile creation, are provided in the Related Records section of this document.
| Ref No | When it is created | How it is created | What change we implemented |
|---|---|---|---|
| AC01 | When they raise a case for themselves (Opened for) | Business rule:Create HR Profile for Users (See RR11 below) | We disabled this rule and created a new one: Create HR Profile for Users from Staging.Instead of creating an HR Profile directly from the sys_user table, which lacks detailed information, the new rule creates profiles using records from the HR Profile Staging table (sn_hr_core_profile_staging). This table contains comprehensive data from the SAP integration, which includes all users at Corrections (i.e., employees and external users (employment_group LIKE 'external')). |
| AC02 | When a case is raised for them (Subject Person) | Business rule: Create HR Profile for Users (See RR11 below) | Refer to AC01 above. |
| AC03 | When the user visits the HR portal (Employee Center Portal) | System property:sn_hr_core.esc.create_hrprofile.override (See RR12 below). | This property was downloaded from ServiceNow and is now set as FALSE by default. |
| AC04 | When fulfillers select the user (not saved) as the Subject Person on HR Case forms in the back-end portal | Client Script:Set HR Profile on subject person change (See RR1 below). | We disabled this script and created a new one: Set HR Profile on subject person change |

Scheduled Job (Daily Run to Process HR Profiles)

Benefits Achieved
By implementing this change, we stabilised our subscription count, reducing it by approximately 10% to 12% from its previous peak. This accurate and consistent count aligns with our actual usage and prevents ServiceNow from capturing inflated subscription snapshots during peak times. This ensures we are accurately billed for our true subscription usage, optimising our ServiceNow performance and cost-efficiency.
Related Records
The following records are the key components that enable this solution to function as intended.
| Ref No | Record name | Record table | Purpose |
|---|---|---|---|
| RR11 | Create HR Profiles for Users from Staging | Business Rule | Profiles of External users are saved in the sn_hr_core_profile_staging table during SAP Integration. Because of this business rule, the OOB BR [Create HR Profile for users] is disabled, allowing HR Profiles to be created from HR Profile Staging (sn_hr_core_profile_staging) instead of User (sys_user) table.To see the original OOB business rule, please check: Create HR Profile for users |
| RR12 | sn_hr_core.esc.create_hrprofile.override | System Property | A system property introduced in the Rome Release (ServiceNow KB0965824 ) needed to be manually imported. This property allows customers to control whether an HR Profile is created during the first visit to the Employee Service Portal (Corrections’ HR Portal). It is crucial to keep this property set to FALSE to maintain the desired behaviour. |
| RR13 | Set HR Profile on subject person change | Client Script | The default Client Script Set HR Profile on subject person change with sys_id 365330019f22120047a2d126c42e70fe has been disabled, and a new Client Script with the same name has been created. The original script automatically created an HR Profile when a fulfiller selected a user without an HR Profile from the subject_person list. However, HR Profiles should only be created through the Business Rule Create HR Profiles for Users from Staging when the HR Case is submitted or edited.The new script, with sys_id a7c816a0337142508c66e6b45d5c7baf, ensures that other fields on the HR Case form display information about the subject person without creating an HR Profile. Additionally, a new Script Include record, hr_case_Ajax_Corrections, was created to support this Client Script. |
| RR14 | subject_person_hr_profile | Dictionary Entry | In the HR Case table (sn_hr_core_case), we need to change the Reference cascade rule value for a specific column from --None-- to None. This ensures that when an HR Profile is moved from the HR Profile table (sn_hr_core_profile) to the HR Profile Staging table (sn_hr_core_profile_staging), the sys_id of the subject person is not deleted from the subject_person field in the HR Case table. |
| RR15 | Enforce unique profile | Business Rule | This is an OOB business rule that checks if the record inserted in sn_hr_core_profile table is unique by checking the following combination:First name + Last name + Personal email address We disabled this business rule because this unique profile combination does not fit with the organisation’s process. Currently, uniqueness is defined by the Employee number and Username. This validation occurs during the transformation process, where the Employee number serves as the primary coalesce key. Additionally, if the username already exists in the sn_hr_core_profile_staging table, it is also checked. This validation persists during the data transfer from the HR Profile Staging table to the HR Profile table (sn_hr_core_profile).Coalesce ensures that this field remains unique. When ServiceNow detects data matching an existing record via the coalesce key, it updates that record instead of creating a duplicate entry. |
| RR16 | hr_Profile | Script Include | The method getProfileFromParameters was updated to check the HR Profile Staging table before creating a new HR Profile. If a user exists in the staging table, the system now creates the profile from staging instead of defaulting to the User record. This helps avoid unnecessary profile creation and licensing costs, especially for contractors. |
Helpful Scripts
Business Rule: Create HR Profile for users from Staging
Table: sn_hr_core_case
Before: Insert
(function executeRule(current, previous /*null when async*/) {
/* This script is created for XX Organisation. Profiles of External users are saved in the sn_hr_core_profile_staging table during SAP Integration.
Because of this business rule, the OOB BR [Create HR Profile for users] is disabled, allowing HR Profiles to be created from Staging instead of SYS_USER table.
To see the original OOB BR, please check: Create HR Profile for users [07da53b40b302200f9fd064d37673ab1].
*/
var userOpenedFor = current.opened_for;
checkAndCreateHRProfile(userOpenedFor);
var userSubjectPerson = current.subject_person;
if (userSubjectPerson) {
checkAndCreateHRProfile(userSubjectPerson);
}
})(current, previous);
function checkAndCreateHRProfile(user) {
var gr = new GlideRecord('sn_hr_core_profile');
gr.addQuery('user', user);
gr.query();
if (!gr.next()) {
var grProfileStaging = new GlideRecord('sn_hr_core_profile_staging');
grProfileStaging.addQuery('user', user);
grProfileStaging.query();
if (grProfileStaging.next()) {
// Create HR Profile from its HR Profile Staging record.
createHRProfileFromStaging(grProfileStaging);
} else {
// If not found in Staging table, call the OOB way of Creating HRProfile.
createHRProfileFromUser(user);
}
}
}
function createHRProfileFromStaging(grProfileStaging) {
var grCreate = new GlideRecord('sn_hr_core_profile');
grCreate.initialize();
for (var field in grProfileStaging) {
if (field !== 'number' && grProfileStaging[field] && typeof grProfileStaging[field] !== 'function') {
grCreate.setValue(field, grProfileStaging[field]);
}
}
grCreate.setNewGuidValue(grProfileStaging.sys_id);
grCreate.insert();
}
function createHRProfileFromUser(user) {
var hrProfile = new hr_Profile();
hrProfile.createProfileFromUser(user);
}
System Properties: sn_hr_core.esc.create_hrprofile.override
Value = false
Script Include: hr_Profile
getProfileFromParameters: function(parameters) {
var user = parameters['opened_for'] || parameters['user'];
var hrProfile = new GlideRecord(hr.TABLE_PROFILE);
var hrProfileStaging = new GlideRecord(hr.TABLE_PROFILE_STAGING);
if (hrProfile.get('user', user)) {
return hrProfile;
} else if (hrProfileStaging.get('user', user)) {
gs.info('hr_Profile: hrProfileStaging');
return new sn_hr_core.hr_Profile_Staging()._createProfileFromStaging(user);
} else {
return this._createProfileFromUser(user, parameters);
}
}
Client Script: Set HR Profile on subject person change
onChange of Subject_Person
Inherited: True
Global: True
Isolate Script: True
UI type: all
function onChange(control, oldValue, newValue, isLoading) {
if (isLoading || !newValue || (typeof g_aw !== 'undefined' && g_form.isNewRecord()))
return;
var ga = new GlideAjax("hr_CaseAjax_Corrections");
ga.addParam("sysparm_name", "createOrGetProfileFromUser");
ga.addParam("sysparm_user", g_form.getValue('subject_person'));
var profileOnForm = g_form.getValue("subject_person_hr_profile");
ga.getXMLAnswer(function(subPersonProfile) {
if (subPersonProfile) {
if (profileOnForm != subPersonProfile)
g_form.setValue("subject_person_hr_profile", subPersonProfile);
} else
g_form.setValue("subject_person_hr_profile", '');
});
}
Scheduled Job: Daily Run to Process HR Profiles
// Stage 1: Mark withdrawn profiles and deactivate users if employment has ended
var grHRProfileWithdrawn = new GlideRecord('sn_hr_core_profile_staging');
grHRProfileWithdrawn.addEncodedQuery('employment_end_date<javascript:gs.beginningOfToday()^u_employee_state=Active');
grHRProfileWithdrawn.query();
while (grHRProfileWithdrawn.next()) {
grHRProfileWithdrawn.u_employee_state = 'Withdrawn';
grHRProfileWithdrawn.update();
var grSetUser = new GlideRecord('sys_user');
if (grSetUser.get('employee_number', grHRProfileWithdrawn.employee_number)) {
grSetUser.active = false;
grSetUser.locked_out = true;
grSetUser.update();
}
}
gs.info('Daily Run: Stage 1 - End');
// Stage 2: Sync staging records to HR Profile table
var grHRProfileStaging = new GlideRecord('sn_hr_core_profile_staging');
grHRProfileStaging.query();
while (grHRProfileStaging.next()) {
var employeeState = grHRProfileStaging.u_employee_state;
var employment_group = String(grHRProfileStaging.u_employment_group || '').toLowerCase();
var grHRProfile = new GlideRecord('sn_hr_core_profile');
grHRProfile.addQuery('sys_id', grHRProfileStaging.sys_id);
grHRProfile.query();
if (grHRProfile.next()) {
for (var field in grHRProfileStaging) {
if (field !== 'number' && typeof grHRProfileStaging[field] !== 'function') {
grHRProfile.setValue(field, grHRProfileStaging[field]);
}
}
grHRProfile.update();
} else {
if (employeeState !== 'Active') {
createHRProfileFromStaging(grHRProfileStaging);
} else if (employment_group.includes('external')) {
var grCases = new GlideRecord('sn_hr_core_case');
grCases.addQuery('opened_for', grHRProfileStaging.user)
.addOrCondition('subject_person_hr_profile', grHRProfileStaging.sys_id);
grCases.addQuery('stateIN1,10,11,18,19,20,24,25');
grCases.setLimit(1);
grCases.query();
if (grCases.hasNext()) {
createHRProfileFromStaging(grHRProfileStaging);
}
} else {
createHRProfileFromStaging(grHRProfileStaging);
}
}
}
gs.info('Daily Run: Stage 2 - End');
// Stage 3: Remove external profiles with no active cases
var grHRProfile = new GlideRecord('sn_hr_core_profile');
grHRProfile.addEncodedQuery([
'user.active=true',
'u_employment_groupLIKEexternal',
'employment_start_date<=javascript:gs.endOfToday()',
'employment_end_date>=javascript:gs.beginningOfToday()'
].join('^'));
grHRProfile.query();
while (grHRProfile.next()) {
var grCases = new GlideRecord('sn_hr_core_case');
grCases.addQuery('opened_for', grHRProfile.user)
.addOrCondition('subject_person_hr_profile', grHRProfile.sys_id);
grCases.addQuery('stateIN1,10,11,18,19,20,24,25');
grCases.setLimit(1);
grCases.query();
if (!grCases.hasNext()) {
grHRProfile.deleteRecord();
}
}
gs.info('Daily Run: Stage 3 - End');
// Helper function to create HR Profile from staging
function createHRProfileFromStaging(grHRProfileStaging) {
var grCreate = new GlideRecord('sn_hr_core_profile');
grCreate.initialize();
for (var field in grHRProfileStaging) {
if (field !== 'number' && typeof grHRProfileStaging[field] !== 'function') {
grCreate.setValue(field, grHRProfileStaging[field]);
}
}
grCreate.setNewGuidValue(grHRProfileStaging.sys_id);
grCreate.insert();
}
Final Thoughts
This article is not designed to circumvent ServiceNow HRSD licensing. Instead, it supports a valid use case for smarter licensing by aligning HR Profile creation with actual system usage. In the organisation where this was applied, the subscription model was consistently exceeding budget due to automatic profile creation for users who never interacted with HR services. Without this optimisation, the licensing costs would have remained unsustainable. By refining the process, the organisation was able to retain necessary data, support operational needs, and ensure that licensing reflects real value.


Leave a Reply