HRSD: updating opened_for in agent workspace

beckerlt
Tera Contributor

Hello,

 

I am working on a project that involves making a UI button visible in the agent workspace based on a field in the hr_profile. During this implementation, I discovered that our opened_for does not always align with our hr_profile.

 

I found some relevant Business Rules that populate these fields (which I believe are OOB)

 

You will see that there is a business rule that updates the subject_person_hr_profile whenever the subject person changes. But the Business rules for hr_profile either set the opened_for based on the hr_profile OR set the hr_profile based on the opened_for.

 

In HR Agent Workspace we do not have an OOB field visible for agents to change the hr_profile. They do however, change the opened_for rather frequently. I looked in our audit table in our production instance and found that the opened_for changes after case creation hundreds of times per year. That means for all of those cases the opened_for and the hr_profile is out of sync.

 

Which is the best approach to resolve this inconsistency? Should I update the BRs to mimic the way the subject_person populates the hr_profile. Or should I surface the hr_profile field in the Agent workspace and make the opened_for read only so the agent has to change the hr_profile directly?

 

1) Populate profile
Before | Order 100 | Insert and Update

Condition: !current.opened_for.nil() && current.hr_profile.nil()
Script:

(function executeRule(current, previous /*null when async*/) {
    var profileGr = new GlideRecord("sn_hr_core_profile");
    profileGr.addQuery("user", current.opened_for);
    profileGr.query();
    if (profileGr.next())
        current.hr_profile = profileGr.getUniqueValue();
})(current, previous);
2) Set opened_for based opened_for_profile
Before | Order 100 | Update

Condition: current.hr_profile.changes()
Script: 

var grProfile = new GlideRecord('sn_hr_core_profile');

if (grProfile.get(current.hr_profile)) {
    var grUser = new GlideRecord('sys_user');
    if (grUser.get(grProfile.user))
        current.setValue('opened_for', grUser.sys_id);
}
 
3) Populate HR Profile For Subject Person
Before | Order 100 | Insert & Update
Condition:  current.subject_person.changes()
Script:
(function executeRule(current, previous /*null when async*/) {
    if (gs.nil(current.subject_person))
        current.subject_person_hr_profile = "";
    else {
        var hrProfile = new GlideRecord('sn_hr_core_profile');
        hrProfile.addQuery('user', current.subject_person);
        hrProfile.query();
        if (hrProfile.next())
            current.subject_person_hr_profile = hrProfile.getUniqueValue();
    }
})(current, previous);
1 ACCEPTED SOLUTION

Naveen20
ServiceNow Employee

BR #1 only populates hr_profile when it's nil, so any subsequent change to opened_for leaves the two fields out of sync. Meanwhile BR #3 shows ServiceNow's own preferred pattern: whenever the person field changes, re-derive the profile.

I'd recommend Option A — add a BR that syncs hr_profile when opened_for changes, mirroring the subject_person pattern. Here's why:

UX disruption is the main argument against Option B. Your agents already have muscle memory around changing opened_for. Making it read-only and forcing them through hr_profile instead introduces training overhead, potential resistance, and a less intuitive workflow — opened_for is a user reference field agents understand, while hr_profile is an abstraction they shouldn't need to think about. The profile should be a derived value, not something agents manage directly.

The BR approach solves it at the platform level without touching the workspace layout at all. You'd essentially refactor BR #1 to behave like BR #3:

// "Sync HR Profile from Opened For"
// Before | Order 50 | Insert & Update
// Condition: current.opened_for.changes()

(function executeRule(current, previous) {
    if (gs.nil(current.opened_for))
        current.hr_profile = "";
    else {
        var hrProfile = new GlideRecord('sn_hr_core_profile');
        hrProfile.addQuery('user', current.opened_for);
        hrProfile.query();
        if (hrProfile.next())
            current.hr_profile = hrProfile.getUniqueValue();
    }
})(current, previous);

The circular dependency is the thing to watch. BR #2 sets opened_for when hr_profile changes, and this new BR sets hr_profile when opened_for changes. That's a potential infinite loop. A few ways to handle it:

First, set the new BR to order 50 (before BR #2 at 100). Since both are "before" BRs operating on the same record in memory, the sequence matters. When an agent changes opened_for, your new BR derives hr_profile at order 50. Then BR #2 sees hr_profile.changes() at order 100 and sets opened_for back — but it should resolve to the same user, so no net conflict. Still, to be safe, you can tighten BR #2's condition:

// Add to BR #2's condition:
current.hr_profile.changes() && !current.opened_for.changes()

This way BR #2 only fires when someone changes the profile without also changing opened_for — meaning it only governs the scenario where hr_profile is changed directly (which today only happens at insert or via integrations).

One more thing to consider: you'll want to handle the case where opened_for is changed to a user who has no HR profile. Your current BR #1 silently leaves hr_profile as-is in that scenario, which would now mean stale data. The pattern above handles it by clearing hr_profile if opened_for is nil, but you may also want to clear it (or log a warning) when the profile lookup returns no results.

For the existing out-of-sync records in production, a one-time fix script querying cases where opened_for and hr_profile.user don't match would clean up the historical data.

View solution in original post

2 REPLIES 2

Naveen20
ServiceNow Employee

BR #1 only populates hr_profile when it's nil, so any subsequent change to opened_for leaves the two fields out of sync. Meanwhile BR #3 shows ServiceNow's own preferred pattern: whenever the person field changes, re-derive the profile.

I'd recommend Option A — add a BR that syncs hr_profile when opened_for changes, mirroring the subject_person pattern. Here's why:

UX disruption is the main argument against Option B. Your agents already have muscle memory around changing opened_for. Making it read-only and forcing them through hr_profile instead introduces training overhead, potential resistance, and a less intuitive workflow — opened_for is a user reference field agents understand, while hr_profile is an abstraction they shouldn't need to think about. The profile should be a derived value, not something agents manage directly.

The BR approach solves it at the platform level without touching the workspace layout at all. You'd essentially refactor BR #1 to behave like BR #3:

// "Sync HR Profile from Opened For"
// Before | Order 50 | Insert & Update
// Condition: current.opened_for.changes()

(function executeRule(current, previous) {
    if (gs.nil(current.opened_for))
        current.hr_profile = "";
    else {
        var hrProfile = new GlideRecord('sn_hr_core_profile');
        hrProfile.addQuery('user', current.opened_for);
        hrProfile.query();
        if (hrProfile.next())
            current.hr_profile = hrProfile.getUniqueValue();
    }
})(current, previous);

The circular dependency is the thing to watch. BR #2 sets opened_for when hr_profile changes, and this new BR sets hr_profile when opened_for changes. That's a potential infinite loop. A few ways to handle it:

First, set the new BR to order 50 (before BR #2 at 100). Since both are "before" BRs operating on the same record in memory, the sequence matters. When an agent changes opened_for, your new BR derives hr_profile at order 50. Then BR #2 sees hr_profile.changes() at order 100 and sets opened_for back — but it should resolve to the same user, so no net conflict. Still, to be safe, you can tighten BR #2's condition:

// Add to BR #2's condition:
current.hr_profile.changes() && !current.opened_for.changes()

This way BR #2 only fires when someone changes the profile without also changing opened_for — meaning it only governs the scenario where hr_profile is changed directly (which today only happens at insert or via integrations).

One more thing to consider: you'll want to handle the case where opened_for is changed to a user who has no HR profile. Your current BR #1 silently leaves hr_profile as-is in that scenario, which would now mean stale data. The pattern above handles it by clearing hr_profile if opened_for is nil, but you may also want to clear it (or log a warning) when the profile lookup returns no results.

For the existing out-of-sync records in production, a one-time fix script querying cases where opened_for and hr_profile.user don't match would clean up the historical data.

Tanushree Maiti
Kilo Patron

Hi @beckerlt 

 

First check this KB :

 KB2656755 HR Cases Missing "Subject Person" and "Opened For" Fields When Created from Agent Workspac... 

HR Agent Workspace At a Glance Issue - Support and Troubleshooting KB2189091 HR Agent Workspace At a... 

 

Probable Solution:

1. If you add subject_person field in Default view of HR Case table , it will be visible to Agent . So your current & future cases-sorted.

2. For Legacy record, you have to run background script/fix ,to sync the value as per your requirement post getting proper approval as it has impact on reporting/ metrices. 

 

 

Please mark this response as Helpful & Accept it as solution if it assisted you with your question.
Regards
Tanushree Maiti
ServiceNow Technical Architect
Linkedin: