Rule 0 — Before any object (Application + Update Set + Studio)
Repeat these checks before creating or editing any application file. This section is also section 00 Update Set — create the capture set first.
- Application picker — top-right globe / application menu → select HR Leave (
x_786686_hr_leave). Never leave Global selected when creating scoped artifacts. - Update Set — All → System Update Sets → Local Update Sets → New:
After submit: open the record → click Make Current. Direct list: {BASE}/sys_update_set_list.doField Value Name HR Leave – BuildApplication HR Leave State In Progress Description Captures all HR Leave lab artifacts - Open Studio — {BASE}/$studio.do → confirm header shows HR Leave.
If artifacts land in Global scope, delete them and recreate under HR Leave. ACLs and events created globally will not behave correctly for this app.
Verify Update Set: Create a test Script Include in Studio → open HR Leave – Build → Related List Customer Updates shows the new file.
Object catalog
Jump to any object. Complete Application Setup → Flow Leave Approval for a working core app; Service Portal+ are extended chapter objects (Portal, Workspace, Report, ATF, REST) with full create steps on this page.
Part 1: Application Setup
Purpose
Creates the scoped HR Leave application container. All other OBJ artifacts belong to scope x_786686_hr_leave and app sys_id e5f5bc9493a303507520703efaba104d on the lab instance.
Navigation path
Direct URL
Prerequisites
- PDI or dev instance with admin + app_creator roles.
- Rule 0 Update Set can be created immediately after app exists.
Form fields
| Field | Value |
|---|---|
| Name | HR Leave |
| Scope name | x_786686_hr_leave |
| Version | 1.0.0 |
| Short description | HR Leave management v2 lab app |
| Package type | Custom application |
| Create application menu | Checked |
| Studio | Checked (default) |
Step-by-step create
- Open Studio.
- Click Create Application.
- Enter every field from the table. Scope must be
x_786686_hr_leave. - Click Create. Studio opens HR Leave workspace.
- Classic verify: Application record → Scope = x_786686_hr_leave.
- Proceed to Rule 0 Update Set → HR Leave – Build → Make Current.
Verify checklist
- Application picker lists HR Leave.
- Scope prefix on new tables is
x_786686_hr_leave_. - Application Menu stub exists (modules added in Modules).
Demo / test
Switch picker Global ↔ HR Leave. New Update Set shows Application = HR Leave.
Part 2: Table — HR Employee (employee_v2)
Purpose
Stores HR employee profiles linked to platform users. BR1 reads u_manager to populate leave request approver.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup Application HR Leave.
- Rule 0 Update Set current.
Table form fields (create the table first)
| Field on New Table form | Suggested professional value |
|---|---|
| Label | HR Employee |
| Name | employee_v2 → full name becomes x_786686_hr_leave_employee_v2 |
| Extends table | (leave empty — do not extend Task) |
| Create module | true (or add later in Modules) |
| Create access controls | true (refine later in Administration) |
| Application | HR Leave |
Where: Studio → Create Application File → Table · Direct: {BASE}/sys_db_object.do
Column form fields (each column New)
On Columns related list → New: set Column label, Type, Max length, Reference. Element name = code below.
Form fields
| Column | Column label | Type | Max length | Reference | Choice values | Display | Mandatory |
|---|---|---|---|---|---|---|---|
u_user | User | Reference | — | sys_user | — | No | Yes |
u_employee_id | Employee ID | String | 40 | — | — | No | Yes |
u_department | Department | String | 100 | — | — | No | No |
u_manager | Manager | Reference | — | sys_user | — | No | Yes |
u_join_date | Join Date | Date | — | — | — | No | No |
u_grade | Grade | String | 20 | — | — | No | No |
Form Layout — x_786686_hr_leave_employee_v2
- All → System Definition → Tables → open
x_786686_hr_leave_employee_v2(or Studio → Tables). - Related link Form Layout → New or edit default.
- Drag fields into sections: Profile (u_user, u_employee_id, u_department, u_grade, u_join_date), Management (u_manager).
- Save.
List Layout — x_786686_hr_leave_employee_v2
- Same table record → related link List Layout.
- Add columns (left to right): User, Employee ID, Department, Manager, Join Date, Grade.
- Save → open list URL and confirm columns appear.
Step-by-step create
- Studio → Create Application File → Table.
- Label:
HR Employee· Name:employee_v2→ full namex_786686_hr_leave_employee_v2. - Extends table: leave blank (standalone).
- Submit table record.
- For each column in the table above: Table → Columns related list → New:
- Element = column name · Column label = friendly name · Type = as listed · Max length · Reference · Application = HR Leave.
- Complete Form Layout and List Layout steps above.
Verify checklist
- List opens: employee_v2 list
- Dictionary:
{BASE}/sys_dictionary_list.do?sysparm_query=name=x_786686_hr_leave_employee_v2^elementSTARTSWITHu_ - Six u_* columns under HR Leave application.
Demo / test
- New record → User = employee1, Employee ID = E-1001, Manager = manager1.
- Save — no mandatory errors.
Part 3: Table — Leave Request (req_v2)
Purpose
Core leave request table. Business rules, Flow, catalog, portal, and REST all target this table.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup Application.
- Part 2: Table employee_v2 (for u_employee_ref reference).
Table form fields (create the table first)
| Field on New Table form | Suggested professional value |
|---|---|
| Label | Leave Request |
| Name | req_v2 → x_786686_hr_leave_req_v2 |
| Extends table | (empty) |
| Create module | true |
| Application | HR Leave |
Where: Studio → Create Application File → Table · New table
Column form fields (each column New)
On Columns related list → New: set Column label, Type, Max length, Reference. Element name = code below.
Form fields
| Column | Column label | Type | Max length | Reference | Choice values | Display | Mandatory |
|---|---|---|---|---|---|---|---|
u_number | Number | String | 40 | — | — | Yes (display) | No |
u_requested_for_user | Requested For | Reference | — | sys_user | — | No | Yes |
u_employee_ref | Employee | Reference | — | x_786686_hr_leave_employee_v2 | — | No | Yes |
u_leave_type | Leave Type | Choice | — | — | annual, sick, unpaid | No | Yes |
u_start_date | Start Date | Date | — | — | — | No | Yes |
u_end_date | End Date | Date | — | — | — | No | Yes |
u_days_count | Days | Integer | — | — | — | No | No (read-only on form) |
u_state | State | Choice | — | — | draft, submitted, approved, rejected, cancelled | No | Yes |
u_approver_user | Approver | Reference | — | sys_user | — | No | No (set by BR1) |
u_reason | Reason | String | 1000 | — | — | No | Conditional (UI Policy sick) |
Choice values — u_leave_type
| Value | Label |
|---|---|
| annual | Annual |
| sick | Sick |
| unpaid | Unpaid |
Choice values — u_state
| Value | Label |
|---|---|
| draft | Draft |
| submitted | Submitted |
| approved | Approved |
| rejected | Rejected |
| cancelled | Cancelled |
Form Layout — x_786686_hr_leave_req_v2
- All → System Definition → Tables → open
x_786686_hr_leave_req_v2(or Studio → Tables). - Related link Form Layout → New or edit default.
- Drag fields into sections: Request (u_number, u_requested_for_user, u_employee_ref, u_leave_type), Dates (u_start_date, u_end_date, u_days_count), Approval (u_approver_user, u_reason), Status (u_state).
- Save.
List Layout — x_786686_hr_leave_req_v2
- Same table record → related link List Layout.
- Add columns (left to right): Number, Requested For, Leave Type, Start Date, End Date, Days, State, Approver.
- Save → open list URL and confirm columns appear.
Step-by-step create
- Studio → Create Application File → Table.
- Label:
Leave Request· Name:req_v2→x_786686_hr_leave_req_v2. - Create all columns per field table. For Choice columns add values in Choice List related list.
- Table record → Display field =
u_number. - Dictionary: mark
u_days_countread-only on form (optional dictionary flag). - Complete Form Layout and List Layout above.
Verify checklist
- Ten u_* columns exist.
- Choice fields show five states and three leave types.
- Reference picker on u_employee_ref shows employee_v2 rows.
Demo / test
New req_v2 as admin: fill dates Mon–Fri, save — u_days_count fills after Business Rule BR1 BR1.
Part 4: Table — Leave Balance (bal_v2)
Purpose
Tracks per-employee leave entitlement and usage by year and leave type. BR3 recalculates on approval.
Navigation path
Direct URL
Prerequisites
- Part 2: Table employee_v2.
- Part 20: System Properties property for default entitled days (can seed manually first).
Table form fields (create the table first)
| Field on New Table form | Suggested professional value |
|---|---|
| Label | Leave Balance |
| Name | bal_v2 → x_786686_hr_leave_bal_v2 |
| Extends table | (empty) |
| Application | HR Leave |
Column form fields (each column New)
On Columns related list → New: set Column label, Type, Max length, Reference. Element name = code below.
Form fields
| Column | Column label | Type | Max length | Reference | Choice values | Display | Mandatory |
|---|---|---|---|---|---|---|---|
u_employee_ref | Employee | Reference | — | x_786686_hr_leave_employee_v2 | — | No | Yes |
u_year | Year | Integer | — | — | — | No | Yes |
u_leave_type | Leave Type | Choice | — | — | annual, sick, unpaid | No | Yes |
u_entitled | Entitled | Integer | — | — | — | No | Yes |
u_used | Used | Integer | — | — | — | No | No |
u_remaining | Remaining | Integer | — | — | — | No | No |
Form Layout — x_786686_hr_leave_bal_v2
- All → System Definition → Tables → open
x_786686_hr_leave_bal_v2(or Studio → Tables). - Related link Form Layout → New or edit default.
- Drag fields into sections: Employee (u_employee_ref, u_year, u_leave_type), Balance (u_entitled, u_used, u_remaining).
- Save.
List Layout — x_786686_hr_leave_bal_v2
- Same table record → related link List Layout.
- Add columns (left to right): Employee, Year, Leave Type, Entitled, Used, Remaining.
- Save → open list URL and confirm columns appear.
Step-by-step create
- Studio → Table → Name
bal_v2. - Add six columns per table.
- Reuse u_leave_type choice values from req_v2.
- Form/List layouts per steps above.
Verify checklist
- bal_v2 list opens.
- Reference to employee_v2 works.
Demo / test
Seed: employee, year 2026, annual, entitled 20, used 0, remaining 20.
Part 5: Table — Leave Audit (audit_v2)
Purpose
Append-only audit trail for leave state changes. BR3 writes rows automatically.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
Table form fields (create the table first)
| Field on New Table form | Suggested professional value |
|---|---|
| Label | Leave Audit |
| Name | audit_v2 → x_786686_hr_leave_audit_v2 |
| Extends table | (empty) |
| Application | HR Leave |
Column form fields (each column New)
On Columns related list → New: set Column label, Type, Max length, Reference. Element name = code below.
Form fields
| Column | Column label | Type | Max length | Reference | Choice values | Display | Mandatory |
|---|---|---|---|---|---|---|---|
u_leave_ref | Leave | Reference | — | x_786686_hr_leave_req_v2 | — | No | Yes |
u_old_state | Old State | String | 40 | — | — | No | No |
u_new_state | New State | String | 40 | — | — | No | Yes |
u_event_type | Event Type | String | 60 | — | — | No | No |
u_actor | Actor | Reference | — | sys_user | — | No | No |
Form Layout — x_786686_hr_leave_audit_v2
- All → System Definition → Tables → open
x_786686_hr_leave_audit_v2(or Studio → Tables). - Related link Form Layout → New or edit default.
- Drag fields into sections: Audit (u_leave_ref, u_event_type, u_old_state, u_new_state, u_actor).
- Save.
List Layout — x_786686_hr_leave_audit_v2
- Same table record → related link List Layout.
- Add columns (left to right): Leave Request, Old State, New State, Event Type, Actor, Created.
- Save → open list URL and confirm columns appear.
Step-by-step create
- Studio → Table →
audit_v2. - Add five columns.
- Layouts as above.
Verify checklist
- audit_v2 list opens.
- Reference to req_v2 works.
Demo / test
After Business Rule BR3 BR3: change req state → new audit row appears.
Part 6: Script Include — HRLXProLeaveUtilsV2
Purpose
Server-side utility for business-day calculation, approver resolution, audit writes, balance recalc, and GlideAjax for CS2.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup.
- Tables Table HR Employee..05 exist.
Form fields
| Field | Value |
|---|---|
| Name | HRLXProLeaveUtilsV2 |
| API Name | x_786686_hr_leave.HRLXProLeaveUtilsV2 |
| Client callable | Checked |
| Active | Checked |
| Accessible from | All application scopes |
| Script | Paste block below |
Extends AbstractAjaxProcessor for ajaxGetBusinessDays.
Step-by-step create
- Studio → Script Include → New.
- Fill form fields table.
- Paste full script below into Script field.
- Save — fix any syntax errors.
Full script (paste-ready)
/**
* Script Include: HRLXProLeaveUtilsV2
* Scope: x_786686_hr_leave
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Client callable: true (extends AbstractAjaxProcessor)
* Purpose: Leave calculations + helpers (business days, approver, balance, audit).
*
* Create in ServiceNow:
* Studio > Create Application File > Script Include
* Name: HRLXProLeaveUtilsV2
* Client callable: checked
*/
var HRLXProLeaveUtilsV2 = Class.create();
HRLXProLeaveUtilsV2.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
/**
* Count business days (Mon-Fri) inclusive between two date strings.
* @param {string} startDate - 'yyyy-MM-dd'
* @param {string} endDate - 'yyyy-MM-dd'
* @return {number}
*/
calculateBusinessDays: function (startDate, endDate) {
if (!startDate || !endDate) return 0;
var start = new GlideDateTime(startDate + ' 00:00:00');
var end = new GlideDateTime(endDate + ' 00:00:00');
if (end.before(start)) return 0;
var days = 0;
var cursor = new GlideDateTime(start);
while (!cursor.after(end)) {
var dow = cursor.getDayOfWeekLocalTime(); // 1=Mon ... 7=Sun
if (dow >= 1 && dow <= 5) days++;
cursor.addDaysLocalTime(1);
}
return days;
},
/**
* Resolve the approver (employee's manager) and set it on the record.
* @param {GlideRecord} currentRecord - leave request record
*/
setApproverFromEmployee: function (currentRecord) {
var empId = currentRecord.getValue('u_employee_ref');
if (!empId) return;
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
if (emp.get(empId) && emp.u_manager) {
currentRecord.u_approver_user = emp.getValue('u_manager');
}
},
/**
* Write an audit row when the state changes.
*/
auditStateChange: function (leaveSysId, oldState, newState, eventType) {
var a = new GlideRecord('x_786686_hr_leave_audit_v2');
a.initialize();
a.u_leave_ref = leaveSysId;
a.u_old_state = oldState || '';
a.u_new_state = newState || '';
a.u_event_type = eventType || 'state_change';
a.u_actor = gs.getUserID();
a.insert();
},
/**
* Recalculate remaining balance for an employee/year.
*/
recalculateBalance: function (employeeSysId, year) {
var used = this._sumApprovedDays(employeeSysId, year);
var bal = new GlideRecord('x_786686_hr_leave_bal_v2');
bal.addQuery('u_employee_ref', employeeSysId);
bal.addQuery('u_year', year);
bal.query();
if (bal.next()) {
bal.u_used = used;
bal.u_remaining = parseInt(bal.getValue('u_entitled') || 0, 10) - used;
bal.update();
}
},
_sumApprovedDays: function (employeeSysId, year) {
var total = 0;
var gr = new GlideAggregate('x_786686_hr_leave_req_v2');
gr.addQuery('u_employee_ref', employeeSysId);
gr.addQuery('u_state', 'approved');
gr.addQuery('u_start_date', '>=', year + '-01-01');
gr.addQuery('u_start_date', '<=', year + '-12-31');
gr.addAggregate('SUM', 'u_days_count');
gr.query();
if (gr.next()) total = parseInt(gr.getAggregate('SUM', 'u_days_count') || 0, 10);
return total;
},
/**
* GlideAjax entry point for client-side day calculation.
*/
ajaxGetBusinessDays: function () {
return this.calculateBusinessDays(
this.getParameter('sysparm_start'),
this.getParameter('sysparm_end')
);
},
type: 'HRLXProLeaveUtilsV2'
});
Verify checklist
- Active record under HR Leave.
- No compile error banner.
- Background test returns 5 for Mon–Fri range.
Demo / test
var util = new x_786686_hr_leave.HRLXProLeaveUtilsV2();
gs.info('Days: ' + util.calculateBusinessDays('2026-09-01', '2026-09-05'));Part 7: Script Include — HRLXProSecurityUtilsV2
Purpose
Centralized read/write authorization logic consumed by table ACLs on req_v2.
Navigation path
Direct URL
Prerequisites
- Part 6: Script Include (same app scope).
Form fields
| Field | Value |
|---|---|
| Name | HRLXProSecurityUtilsV2 |
| Client callable | Unchecked |
| Active | Checked |
Step-by-step create
- Studio → Script Include → New.
- Name = HRLXProSecurityUtilsV2, Client callable = false.
- Paste script below. Save.
Full script (paste-ready)
/**
* Script Include: HRLXProSecurityUtilsV2
* Scope: x_786686_hr_leave
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Client callable: false
* Purpose: Centralized security checks used by ACLs.
*
* Used by ACL scripts, e.g.:
* answer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canReadLeave(current);
*/
var HRLXProSecurityUtilsV2 = Class.create();
HRLXProSecurityUtilsV2.prototype = {
initialize: function () {
this.userId = gs.getUserID();
},
/** HR admin can do anything. */
isHrAdmin: function () {
return gs.hasRole('x_786686_hr_leave.hr_admin');
},
/** Can the current user READ this leave request? */
canReadLeave: function (current) {
if (this.isHrAdmin()) return true;
// owner
if (current.getValue('u_requested_for_user') === this.userId) return true;
// approver / manager
if (current.getValue('u_approver_user') === this.userId) return true;
// manager of the employee
return this._isManagerOfEmployee(current.getValue('u_employee_ref'));
},
/** Can the current user WRITE this leave request? */
canWriteLeave: function (current) {
if (this.isHrAdmin()) return true;
var state = current.getValue('u_state');
// owner can edit only while draft/submitted
if (current.getValue('u_requested_for_user') === this.userId) {
return (state === 'draft' || state === 'submitted');
}
// approver can act when submitted
if (current.getValue('u_approver_user') === this.userId) {
return (state === 'submitted');
}
return false;
},
_isManagerOfEmployee: function (employeeSysId) {
if (!employeeSysId) return false;
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
if (emp.get(employeeSysId)) {
return emp.getValue('u_manager') === this.userId;
}
return false;
},
type: 'HRLXProSecurityUtilsV2'
};
Verify checklist
- Record active under HR Leave.
- Used by ACLs ACL scripts.
Demo / test
Background: instantiate and call canReadLeave on a sample req_v2 GlideRecord.
Part 8: Business Rule BR1 — Calculate Days
Purpose
Calculates u_days_count and sets u_approver_user from employee manager before save.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 6: Script Include HRLXProLeaveUtilsV2.
Form fields
| Field | Value |
|---|---|
| Name | HRLX BR1 Calculate Days |
| Table | x_786686_hr_leave_req_v2 |
| Advanced | Checked |
| When | before |
| Insert | Checked |
| Update | Checked |
| Delete | Unchecked |
| Order | 100 |
| Active | Checked |
Step-by-step create
- Studio → Business Rule → New.
- Fill every field from table. Check Advanced.
- Paste script below into Script field.
- Save. Confirm Active = true.
Full script (paste-ready)
/**
* Business Rule: BR1 - Calculate Days + Approver
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* When: before
* Insert: true Update: true
* Order: 100
*
* Calculates u_days_count and resolves the approver before saving.
*/
(function executeRule(current, previous /*null when inserting*/) {
var util = new x_786686_hr_leave.HRLXProLeaveUtilsV2();
if (current.u_start_date.changes() || current.u_end_date.changes() || current.isNewRecord()) {
current.u_days_count = util.calculateBusinessDays(
current.getValue('u_start_date'),
current.getValue('u_end_date')
);
}
if (current.u_employee_ref.changes() || current.isNewRecord()) {
util.setApproverFromEmployee(current);
}
})(current, previous);
Verify checklist
- BR order: 100, 200, 250, 300.
- All Active on req_v2 under HR Leave.
Demo / test
See section demo after BR is active (days calc, events, audit, balance).
Part 9: Business Rule BR2 — On Submit Event
Purpose
Queues submitted event when u_state becomes submitted (notifications + Flow trigger).
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 6: Script Include HRLXProLeaveUtilsV2.
Form fields
| Field | Value |
|---|---|
| Name | HRLX BR2 On Submit Event |
| Table | x_786686_hr_leave_req_v2 |
| Advanced | Checked |
| When | after |
| Insert | Checked |
| Update | Checked |
| Delete | Unchecked |
| Order | 200 |
| Active | Checked |
Step-by-step create
- Studio → Business Rule → New.
- Fill every field from table. Check Advanced.
- Paste script below into Script field.
- Save. Confirm Active = true.
Full script (paste-ready)
/**
* Business Rule: BR2 - On Submit Fire Event
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* When: after
* Update: true Insert: true
* Order: 200
*
* Fires an event when state becomes 'submitted' (drives notification / flow).
*/
(function executeRule(current, previous) {
if (current.u_state.changesTo('submitted')) {
gs.eventQueue(
'x_786686_hr_leave.leave_v2.submitted',
current,
current.getValue('u_approver_user'),
current.getValue('u_number')
);
}
})(current, previous);
Verify checklist
- BR order: 100, 200, 250, 300.
- All Active on req_v2 under HR Leave.
Demo / test
See section demo after BR is active (days calc, events, audit, balance).
Part 10: Business Rule BR3 — Audit State Change
Purpose
Writes audit_v2 row on state change; recalculates balance on approval.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 6: Script Include HRLXProLeaveUtilsV2.
Form fields
| Field | Value |
|---|---|
| Name | HRLX BR3 Audit State Change |
| Table | x_786686_hr_leave_req_v2 |
| Advanced | Checked |
| When | after |
| Insert | Unchecked |
| Update | Checked |
| Delete | Unchecked |
| Order | 300 |
| Active | Checked |
Step-by-step create
- Studio → Business Rule → New.
- Fill every field from table. Check Advanced.
- Paste script below into Script field.
- Save. Confirm Active = true.
Full script (paste-ready)
/**
* Business Rule: BR3 - Audit State Change + Balance
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* When: after
* Update: true
* Order: 300
*
* Writes an audit row on any state change and recalculates balance on approval.
*/
(function executeRule(current, previous) {
if (!current.u_state.changes()) return;
var util = new x_786686_hr_leave.HRLXProLeaveUtilsV2();
util.auditStateChange(
current.getUniqueValue(),
previous ? previous.getValue('u_state') : '',
current.getValue('u_state'),
'state_change'
);
if (current.u_state.changesTo('approved') && current.u_employee_ref) {
var year = new GlideDateTime(current.getValue('u_start_date')).getYearLocalTime();
util.recalculateBalance(current.getValue('u_employee_ref'), year);
}
})(current, previous);
Verify checklist
- BR order: 100, 200, 250, 300.
- All Active on req_v2 under HR Leave.
Demo / test
See section demo after BR is active (days calc, events, audit, balance).
Part 11: Business Rule BR4 — Approve / Reject Event
Purpose
Queues approved/rejected events for notifications.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 6: Script Include HRLXProLeaveUtilsV2.
Form fields
| Field | Value |
|---|---|
| Name | HRLX BR4 Approve Reject Event |
| Table | x_786686_hr_leave_req_v2 |
| Advanced | Checked |
| When | after |
| Insert | Unchecked |
| Update | Checked |
| Delete | Unchecked |
| Order | 250 |
| Active | Checked |
Step-by-step create
- Studio → Business Rule → New.
- Fill every field from table. Check Advanced.
- Paste script below into Script field.
- Save. Confirm Active = true.
Full script (paste-ready)
/**
* Business Rule: BR4 - Fire Approved / Rejected Events
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* When: after
* Update: true
* Insert: false
* Order: 250
*
* Drives notifications for approved and rejected leave requests.
* Requires events registered in Event Registry (see events.md).
*/
(function executeRule(current, previous) {
if (current.u_state.changesTo('approved')) {
gs.eventQueue(
'x_786686_hr_leave.leave_v2.approved',
current,
current.getValue('u_requested_for_user'),
current.getValue('u_number')
);
}
if (current.u_state.changesTo('rejected')) {
gs.eventQueue(
'x_786686_hr_leave.leave_v2.rejected',
current,
current.getValue('u_requested_for_user'),
current.getValue('u_number')
);
}
})(current, previous);
Verify checklist
- BR order: 100, 200, 250, 300.
- All Active on req_v2 under HR Leave.
Demo / test
See section demo after BR is active (days calc, events, audit, balance).
Part 12: Client Script CS1 — Validate Dates
Purpose
Client-side validation: end date cannot precede start date on req_v2 form.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2 table/form.
Form fields
| Field | Value |
|---|---|
| Name | HRLX CS1 Validate Dates |
| Table | x_786686_hr_leave_req_v2 |
| Type | onChange |
| Field name | u_end_date |
| UI Type | Desktop + All checked |
| Active | Checked |
Step-by-step create
- Studio → Client Script → New.
- Fill fields. Type = onChange, Field = u_end_date.
- Paste script. Save.
Full script (paste-ready)
/**
* Client Script: CS1 - Validate Dates
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* Type: onChange
* Field: u_end_date
*
* Shows an error if end date is before start date.
*/
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') return;
var start = g_form.getValue('u_start_date');
var end = g_form.getValue('u_end_date');
if (!start || !end) return;
if (end < start) {
g_form.showFieldMsg('u_end_date', getMessage('End date cannot be before start date'), 'error');
g_form.clearValue('u_end_date');
} else {
g_form.hideFieldMsg('u_end_date', true);
}
}
Verify checklist
- Client script list shows Active on req_v2.
Demo / test
Start 2026-09-10, End 2026-09-01 → error, end cleared.
Part 13: Client Script CS2 — Calc Days (GlideAjax)
Purpose
Calls HRLXProLeaveUtilsV2.ajaxGetBusinessDays to preview u_days_count before save.
Navigation path
Direct URL
Prerequisites
- Part 6: Script Include client-callable Script Include.
- Part 12: Client Script CS1 can share u_end_date onChange.
Form fields
| Field | Value |
|---|---|
| Name | HRLX CS2 Calc Days GlideAjax |
| Table | x_786686_hr_leave_req_v2 |
| Type | onChange |
| Field name | u_end_date (duplicate CS for u_start_date if desired) |
| UI Type | Desktop + All |
| Active | Checked |
Step-by-step create
- Create Client Script after Script Include HRLXProLeaveUtilsV2.
- onChange on u_end_date — same field as CS1 (both execute).
- Paste script. Save.
Full script (paste-ready)
/**
* Client Script: CS2 - Calculate Days (GlideAjax)
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Table: x_786686_hr_leave_req_v2
* Type: onChange
* Field: u_end_date
*
* Calls HRLXProLeaveUtilsV2.ajaxGetBusinessDays asynchronously to fill u_days_count.
*/
function onChange(control, oldValue, newValue, isLoading, isTemplate) {
if (isLoading || newValue === '') return;
var start = g_form.getValue('u_start_date');
var end = g_form.getValue('u_end_date');
if (!start || !end || end < start) return;
var ga = new GlideAjax('x_786686_hr_leave.HRLXProLeaveUtilsV2');
ga.addParam('sysparm_name', 'ajaxGetBusinessDays');
ga.addParam('sysparm_start', start);
ga.addParam('sysparm_end', end);
ga.getXMLAnswer(function (answer) {
if (answer !== null && answer !== '') {
g_form.setValue('u_days_count', answer);
}
});
}
Verify checklist
- Browser dev tools → Network → GlideAjax to HRLXProLeaveUtilsV2.
Demo / test
Valid Mon–Fri range → u_days_count populates before save.
Part 14: UI Policy — Sick Requires Reason
Purpose
When leave type is sick, makes u_reason mandatory on the req_v2 form.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2 with u_leave_type and u_reason.
Form fields
| Field | Value |
|---|---|
| Table | x_786686_hr_leave_req_v2 |
| Short description | Sick leave requires reason |
| Application | HR Leave |
| Active | Checked |
| Reverse if false | Checked |
| On load | Checked |
| Condition | u_leave_type is sick |
UI Policy Action (related list New)
| Field | Value |
|---|---|
| Field | u_reason |
| Mandatory | true |
| Visible | true |
Step-by-step create
- Application = HR Leave.
- All → System UI → UI Policies → New.
- Fill policy fields. Condition builder: Leave Type is sick.
- Submit policy.
- Related list UI Policy Actions → New → u_reason mandatory true.
- Save action. Verify policy Active.
Verify checklist
- Leave type Sick → red asterisk on Reason.
- Annual → Reason not mandatory.
Demo / test
Select Sick, leave Reason empty, try Save → client mandatory warning.
Part 15: UI Action — Submit
Purpose
Form button that moves draft leave requests to submitted state (triggers BR2 and Flow).
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 9: Business Rule BR2 BR2 for event.
Form fields
| Field | Value |
|---|---|
| Name | Submit |
| Table | x_786686_hr_leave_req_v2 |
| Action name | submit_leave |
| Form button | Checked |
| Form style | Primary (optional) |
| Order | 100 |
| Condition | current.u_state == 'draft' |
| Client | Unchecked (server script) |
| Active | Checked |
Step-by-step create
- Studio → UI Action → New on req_v2.
- Fill fields. Uncheck Client checkbox.
- Paste server script below in Script field.
- Save. Open draft req_v2 → Submit button visible.
Full script (paste-ready)
current.u_state = 'submitted';
current.update();
action.setRedirectURL(current);
Verify checklist
- Button only on draft records.
- After click state = submitted.
Demo / test
Draft → Submit → BR2 event + Flow Flow Leave Approval starts.
Part 16: Roles
Purpose
Defines four scoped roles for employee self-service, manager approval, HR admin, and integration REST access.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup Application HR Leave.
Form fields
| Name | Description | Assignable by |
|---|---|---|
x_786686_hr_leave.employee | Submit and view own leave | hr_admin |
x_786686_hr_leave.manager | Approve team leave | hr_admin |
x_786686_hr_leave.hr_admin | Full HR Leave admin | admin |
x_786686_hr_leave.integration | REST / integration user | admin |
Step-by-step create
- For each row: Studio → Role → New.
- Name = scoped role name (prefix auto). Description as table. Application = HR Leave.
- Submit. Repeat four times.
Verify checklist
- Four roles filtered by x_786686_hr_leave.
Demo / test
Assign employee role to test user → effective after group assignment Groups.
Part 17: Groups
Purpose
Assignment groups that grant scoped roles to lab users (employees, managers, HR admins, integration bot).
Navigation path
Direct URL
Prerequisites
- Part 16: Roles roles exist.
Form fields
| Group name | Group Roles | Members |
|---|---|---|
| HRLX Employees | x_786686_hr_leave.employee | employee1 |
| HRLX Managers | x_786686_hr_leave.manager, employee | manager1 |
| HRLX HR Admins | x_786686_hr_leave.hr_admin | hradmin |
| HRLX Integration | x_786686_hr_leave.integration, rest_service | hrlx.integration |
Step-by-step create
- Groups → New → Name from table.
- Related list Group Roles → Edit → add scoped roles.
- Related list Users → add members (create users in Users if missing).
- Repeat for all four groups.
Verify checklist
- User record → Roles related list shows inherited group roles.
Demo / test
Impersonate employee1 → roles include employee.
Part 18: Users (lab matrix)
Purpose
Four lab users for E2E testing: employee, manager, HR admin, integration service account.
Navigation path
Direct URL
Prerequisites
- Part 17: Groups groups ready.
Form fields
| User ID | Name | Group | |
|---|---|---|---|
| employee1 | Lab Employee | [email protected] | HRLX Employees |
| manager1 | Lab Manager | [email protected] | HRLX Managers |
| hradmin | Lab HR Admin | [email protected] | HRLX HR Admins |
| hrlx.integration | Integration Bot | [email protected] | HRLX Integration |
Step-by-step create
- Users → New for each row. Set password per instance policy.
- Active = true. Email required for notifications Notifications.
- Add to group via Groups related list or group Members list.
- Seed employee_v2: employee1 user, manager = manager1, Employee ID E-1001.
Verify checklist
- Impersonate each user successfully.
- employee_v2 links employee1 → manager1.
Demo / test
employee1 creates req_v2 → u_approver_user = manager1 after save (BR1).
Part 19: ACLs
Purpose
Record and field ACLs enforce who can read, write, and create leave requests using HRLXProSecurityUtilsV2.
Navigation path
Direct URL
Prerequisites
- Part 7: Script Include HRLXProSecurityUtilsV2.
- Part 16: Roles roles.
- Elevate security_admin before creating ACLs.
Form fields
ACL 1 — read
| Field | Value |
|---|---|
| Type | record |
| Operation | read |
| Name | x_786686_hr_leave_req_v2 |
| Advanced | Checked |
| Script | answer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canReadLeave(current); |
ACL 2 — write
| Operation | write |
| Script | answer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canWriteLeave(current); |
ACL 3 — create
| Operation | create |
| Requires role | x_786686_hr_leave.employee |
| Script | answer = gs.hasRole('x_786686_hr_leave.employee') || gs.hasRole('x_786686_hr_leave.hr_admin') || gs.hasRole('x_786686_hr_leave.integration'); |
ACL 4 — field write u_approver_user
| Name | x_786686_hr_leave_req_v2.u_approver_user |
| Operation | write |
| Script | answer = gs.hasRole('x_786686_hr_leave.hr_admin'); |
Step-by-step create
- Elevate security_admin. Application = HR Leave.
- ACL → New for each table above. Active = true.
- Paste scripts. Save all four.
Verify checklist
- employee1 sees own requests only.
- manager1 sees direct reports.
- hradmin sees all.
- employee1 direct URL to another user's record → denied.
Demo / test
Impersonate employee1 → open coworker sys_id → Access denied.
Part 20: System Properties
Purpose
Scoped configuration for entitlement, notifications, notice rules, ERP endpoint, and integration user. Create all properties below (not only annual_days) for a professional HR app.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup Application selected in picker.
Form fields — create each row (Name / Type / Value / Description)
| Name | Type | Value | Description |
|---|---|---|---|
x_786686_hr_leave.config.annual_days | integer | 20 | Default annual entitlement |
x_786686_hr_leave.config.sick_days | integer | 10 | Default sick entitlement |
x_786686_hr_leave.config.notify_enabled | true|false | true | Master email switch |
x_786686_hr_leave.config.min_notice_days | integer | 3 | Min days before start (optional BR) |
x_786686_hr_leave.config.max_days_per_request | integer | 30 | Max days per request |
x_786686_hr_leave.config.erp_endpoint | string | https://webhook.site/… | Outbound ERP URL (no secrets) |
x_786686_hr_leave.config.integration_user | string | hrlx.integration | Inbound API user id |
x_786686_hr_leave.config.number_prefix | string | HRLX | u_number prefix |
x_786686_hr_leave.config.portal_page_id | string | hrlx_home | Service Portal page id |
On every property form also set: Application = HR Leave · Ignore cache = false · Private = false.
Step-by-step create
- Application picker = HR Leave.
- Open New property.
- Fill Name, Type, Value, Description from one row → Submit.
- Repeat for all 9 rows. Confirm list: filter list.
Verify checklist
- Background:
gs.info(gs.getProperty('x_786686_hr_leave.config.annual_days'));→ 20. - Nine properties visible in scoped list.
Demo / test
Run Scheduled Job accrual job → new balances use annual_days for u_entitled. Change value to 25 → new rows use 25.
Part 21: Event Registry
Purpose
Registers three scoped events queued by BR2/BR4 for notifications and monitoring.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2 table.
Form fields
| Event name | Table | Active |
|---|---|---|
x_786686_hr_leave.leave_v2.submitted | x_786686_hr_leave_req_v2 | true |
x_786686_hr_leave.leave_v2.approved | x_786686_hr_leave_req_v2 | true |
x_786686_hr_leave.leave_v2.rejected | x_786686_hr_leave_req_v2 | true |
Step-by-step create
- Registry → New (three times).
- Event name exactly as table. Table = Leave Request. Application = HR Leave. Active.
Verify checklist
- Submit req → System Logs → Events shows leave_v2.submitted.
Demo / test
Event parm1 = approver sys_id, parm2 = u_number.
Part 22: Notifications
Purpose
Three email notifications: submitted (to approver), approved/rejected (to requester).
Navigation path
Direct URL
Prerequisites
- Part 21: Event Registry events.
- Part 9: Business Rule BR2 BR2, Part 11: Business Rule BR4 BR4.
- Users have email addresses.
Form fields
Notification 1 — Submitted
| Field | Value |
|---|---|
| Name | HRLX Leave Submitted |
| Table | x_786686_hr_leave_req_v2 |
| Active | true |
| When to send | Event is fired |
| Event name | x_786686_hr_leave.leave_v2.submitted |
| Users/Groups in fields | u_approver_user |
| Subject | Leave ${u_number} needs your approval |
Notification 2 — Approved
| Event | leave_v2.approved |
| Users/Groups in fields | u_requested_for_user |
| Subject | Your leave ${u_number} was approved |
Notification 3 — Rejected
| Event | leave_v2.rejected |
| Users/Groups in fields | u_requested_for_user |
| Subject | Your leave ${u_number} was rejected |
Step-by-step create
- Application = HR Leave. Notifications → New for each.
- Tab When to send: Event is fired + event name.
- Tab Who will receive: Users/Groups in fields → pick u_approver_user or u_requested_for_user.
- Tab What it will contain: Subject + Message HTML with ${u_number}, ${u_start_date}, ${URI_REF}.
- Submit each notification. Active = true.
Verify checklist
- Three active notifications on Leave Request table.
- Email log entry after submit/approve.
Demo / test
Submit as employee1 → manager1 receives email or sys_email row.
Part 23: Scheduled Job — Annual Accrual
Purpose
Creates missing bal_v2 rows for current year for every employee using property annual_days.
Navigation path
Direct URL
Prerequisites
- Part 2: Table employee_v2, Part 4: Table bal_v2, Part 20: System Properties property.
Form fields
| Field | Value |
|---|---|
| Name | HRLX Annual Accrual |
| Application | HR Leave |
| Active | Checked |
| Run | Daily 02:00 or Yearly Jan 1 |
| Run as | System (default) |
Step-by-step create
- Scheduled Script Execution → New.
- Fill fields. Paste script below.
- Save. Click Execute Now to test.
Full script (paste-ready)
/**
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Scheduled Job: Annual Leave Accrual
* Run: yearly (e.g. Jan 1)
*
* Creates a fresh balance row per employee for the new year if missing.
* Default entitlement is read from a property (fallback 21 annual days).
*/
(function annualAccrual() {
var year = new GlideDateTime().getYearLocalTime();
var annualEntitlement = parseInt(gs.getProperty('x_786686_hr_leave.config.annual_days', '21'), 10);
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
emp.query();
var created = 0;
while (emp.next()) {
// skip if a balance for this year/type already exists
var existing = new GlideRecord('x_786686_hr_leave_bal_v2');
existing.addQuery('u_employee_ref', emp.getUniqueValue());
existing.addQuery('u_year', year);
existing.addQuery('u_leave_type', 'annual');
existing.query();
if (existing.hasNext()) continue;
var bal = new GlideRecord('x_786686_hr_leave_bal_v2');
bal.initialize();
bal.u_employee_ref = emp.getUniqueValue();
bal.u_year = year;
bal.u_leave_type = 'annual';
bal.u_entitled = annualEntitlement;
bal.u_used = 0;
bal.u_remaining = annualEntitlement;
bal.insert();
created++;
}
gs.info('[HRLX] Annual accrual ' + year + ' created ' + created + ' balance rows.');
})();
Verify checklist
- Execute Now creates bal_v2 per employee without duplicate year row.
- System log: [HRLX] Annual accrual message.
Demo / test
New employee → run job → u_entitled=20, u_remaining=20.
Part 24: Application Menu
Purpose
Top-level Application Navigator entry for HR Leave modules.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup with Create application menu checked.
Form fields
| Field | Value |
|---|---|
| Title | HR Leave |
| Application | HR Leave |
| Order | 100 |
| Roles | x_786686_hr_leave.employee (minimum) |
Step-by-step create
- If missing: Studio → Application Menu → New → fill fields.
- Save. Confirm appears in Application Navigator.
Verify checklist
- Navigator shows HR Leave heading.
Demo / test
employee1 login → sees HR Leave menu after modules Modules.
Part 25: Modules
Purpose
Four list modules linking to req_v2, employee_v2, bal_v2, audit_v2 tables.
Navigation path
Direct URL
Prerequisites
- Part 24: Application Menu menu. Tables Table HR Employee..05.
Form fields
| Title | Type | Table | Order | Roles |
|---|---|---|---|---|
| Leave Requests | List | x_786686_hr_leave_req_v2 | 10 | employee, manager, hr_admin |
| Employees | List | x_786686_hr_leave_employee_v2 | 20 | hr_admin |
| Balances | List | x_786686_hr_leave_bal_v2 | 30 | hr_admin |
| Audit Trail | List | x_786686_hr_leave_audit_v2 | 40 | hr_admin |
Step-by-step create
- Module → New for each row. Application Menu = HR Leave.
- Type = List → Table = as listed. Order as table.
- Roles: restrict Employees/Balances/Audit to hr_admin; Requests to all three roles.
Verify checklist
- Four modules under HR Leave menu.
- URLs x_786686_hr_leave_*_list.do open.
Demo / test
employee1 → only Leave Requests visible.
Part 26: Catalog Record Producer
Purpose
Service Catalog item that inserts submitted req_v2 from catalog variables.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Part 16: Roles employee role.
- Catalog category HR Leave (create under Service Catalog → Categories).
Form fields
| Field | Value |
|---|---|
| Name | Submit Leave Request |
| Table | x_786686_hr_leave_req_v2 |
| Category | HR Leave |
| Roles | x_786686_hr_leave.employee |
| Active | true |
Variables (related list)
| Name | Type | Question | Mandatory |
|---|---|---|---|
| leave_type | Choice | Leave Type | Yes |
| start_date | Date | Start Date | Yes |
| end_date | Date | End Date | Yes |
| reason | Multi Line Text | Reason | No |
Step-by-step create
- Record Producers → New. Fill header fields.
- Variables related list → New for each variable (names must match producer.* in script).
- Script tab → paste producer script below.
- Catalog → Publish. Test on Service Portal catalog.
Full script (paste-ready)
/**
* Record Producer Script: Submit Leave Request
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Target table: x_786686_hr_leave_req_v2
*
* Maps producer variables to record fields. Runs server-side when submitted.
* Variables expected: leave_type, start_date, end_date, reason
*/
(function () {
current.u_requested_for_user = gs.getUserID();
current.u_leave_type = producer.leave_type;
current.u_start_date = producer.start_date;
current.u_end_date = producer.end_date;
current.u_reason = producer.reason;
current.u_state = 'submitted';
// Link to employee profile of the current user (if exists)
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
emp.addQuery('u_user', gs.getUserID());
emp.query();
if (emp.next()) {
current.u_employee_ref = emp.getUniqueValue();
}
// BR1 will calculate days and set the approver on insert.
})();
Verify checklist
- Catalog item visible to employee role.
- Submit creates req_v2 in submitted state.
Demo / test
employee1 submits → req_v2 row, BR1 sets days + approver, Flow starts.
Part 27: Flow Designer — Leave Approval
Purpose
When leave state becomes submitted, sends approval to u_approver_user; on approve/reject updates u_state.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2 with u_state, u_approver_user.
- Part 8: Business Rule BR1 BR1 fills approver.
- manager1 user for approval tests.
Form fields — Create Flow wizard
| Field | Value |
|---|---|
| Name | HRLX Leave Approval |
| Application | HR Leave |
| Description | Approval flow for submitted leave requests |
Trigger — Record Updated
| Field | Value |
|---|---|
| Trigger type | Record Updated |
| Table | x_786686_hr_leave_req_v2 |
| Condition | State [u_state] is submitted AND State changes |
| Run flow | Once per record (recommended) |
Action 1 — Ask For Approval
| Input | Value |
|---|---|
| Record | Trigger → Leave Request Record (data pill) |
| Table | x_786686_hr_leave_req_v2 |
| Approval field | u_approver_user |
| Approval reason | Leave approval required |
| Rules → Approve | Anyone approves (required — do not leave empty) |
If Approved → Update Record
| Record | Trigger record pill |
| Field map | u_state = approved |
Else → Update Record
| Field map | u_state = rejected |
Step-by-step create
- Flow Designer → Create Flow → Name HRLX Leave Approval, Application HR Leave.
- Add trigger → Record Updated → table req_v2 → condition u_state is submitted + changes.
- + → Ask for Approval → fill Action 1 table → Rules: Anyone approves → Done.
- + → Flow Logic → If → condition "approval was Approved".
- Yes branch → Update Record → u_state = approved.
- Else branch → Update Record → u_state = rejected.
- Save → fix any red errors (missing approval rule is common).
- Test → pick sample submitted req_v2 → Run test.
- Publish → Publish new version → wait for Published status.
Verify checklist
- Submitted req creates sysapproval_approver for manager.
- Approve → u_state approved → BR3 audit + BR4 event.
- Reject → u_state rejected.
Demo / test
- employee1 submits leave (UI Action or catalog).
- manager1 → My Approvals → Approve.
- Verify state, audit_v2, bal_v2 updated.
Part 28: Service Portal Widgets & Page
Purpose
HR Leave portal page with two widgets: my leave requests list and current-year balance cards.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2, Part 4: Table bal_v2, Part 16: Roles employee role.
Form fields — Portal (sp_portal) if creating new
| Field | Value |
|---|---|
| Title | HR Leave Portal |
| URL suffix | hrlx |
| Application | HR Leave |
Page (sp_page)
| Field | Value |
|---|---|
| ID | hrlx_home |
| Title | HR Leave Home |
| Application | HR Leave |
Widget 1 — hrlx_my_leaves
| Field | Value |
|---|---|
| ID | hrlx_my_leaves |
| Name | HRLX My Leaves |
| Client controller | HrlxMyLeavesCtrl (as in client script) |
Widget 2 — hrlx_balance
| Field | Value |
|---|---|
| ID | hrlx_balance |
| Name | HRLX Balance |
| Client controller | HrlxBalanceCtrl |
Step-by-step create
- Portal: Service Portal → Portals → New OR use existing Service Portal → set url_suffix
hrlxon dedicated portal record scoped to HR Leave. - Widget hrlx_my_leaves: SP → Widgets → New → ID hrlx_my_leaves → paste HTML, CSS, Client, Server from sections below.
- Widget hrlx_balance: Repeat for hrlx_balance.
- Page: SP → Pages → New → ID hrlx_home → drag both widgets onto 12-column layout (balance top, my leaves below).
- Container/page access: Page → security / roles → x_786686_hr_leave.employee.
- Menu (optional): SP → Menus → add link to ?id=hrlx_home.
- Save all. Open
{BASE}/hrlx?id=hrlx_homeas employee1.
Full script — hrlx_my_leaves HTML template
<div class="hrlx-my-leaves">
<h3>My Leave Requests</h3>
<table class="table table-striped" ng-if="data.leaves.length">
<thead>
<tr>
<th>Number</th>
<th>Type</th>
<th>Start</th>
<th>End</th>
<th>Days</th>
<th>State</th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in data.leaves">
<td>{{item.number}}</td>
<td>{{item.type}}</td>
<td>{{item.start}}</td>
<td>{{item.end}}</td>
<td>{{item.days}}</td>
<td><span class="label label-default">{{item.state}}</span></td>
<td>
<button class="btn btn-xs btn-danger"
ng-if="item.state === 'submitted' || item.state === 'draft'"
ng-click="c.cancelLeave(item)">Cancel</button>
</td>
</tr>
</tbody>
</table>
<p ng-if="!data.leaves.length" class="text-muted">No leave requests yet.</p>
</div>
Full script — hrlx_my_leaves CSS
.hrlx-my-leaves {
padding: 16px;
h3 {
margin-top: 0;
margin-bottom: 16px;
font-weight: 600;
}
.label {
text-transform: capitalize;
}
}
Full script — hrlx_my_leaves Client script
/**
* Widget: hrlx_my_leaves — Client Controller
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
*/
function HrlxMyLeavesCtrl() {
var c = this;
c.cancelLeave = function (item) {
if (!confirm('Cancel leave ' + item.number + '?')) return;
c.data.action = 'cancel';
c.data.sys_id = item.sys_id;
c.server.update().then(function () {
c.data.action = '';
c.data.sys_id = '';
});
};
}
Full script — hrlx_my_leaves Server script
/**
* Widget: hrlx_my_leaves — Server Script
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Shows the current user's leave requests (secure).
*/
(function () {
data.leaves = [];
var gr = new GlideRecordSecure('x_786686_hr_leave_req_v2');
gr.addQuery('u_requested_for_user', gs.getUserID());
gr.orderByDesc('sys_created_on');
gr.setLimit(50);
gr.query();
while (gr.next()) {
data.leaves.push({
sys_id: gr.getUniqueValue(),
number: gr.getValue('u_number'),
type: gr.getDisplayValue('u_leave_type'),
start: gr.getValue('u_start_date'),
end: gr.getValue('u_end_date'),
days: gr.getValue('u_days_count'),
state: gr.getValue('u_state')
});
}
// Cancel action from client
if (input && input.action === 'cancel' && input.sys_id) {
var c = new GlideRecord('x_786686_hr_leave_req_v2');
if (c.get(input.sys_id) && c.getValue('u_requested_for_user') === gs.getUserID()) {
c.u_state = 'cancelled';
c.update();
}
}
})();
Full script — hrlx_balance HTML template
<div class="hrlx-balance">
<h3>Leave Balance {{data.year}}</h3>
<div class="hrlx-cards" ng-if="data.balances.length">
<div class="hrlx-card" ng-repeat="b in data.balances">
<div class="hrlx-card-type">{{b.type}}</div>
<div class="hrlx-card-remaining">{{b.remaining}}</div>
<div class="hrlx-card-meta">used {{b.used}} / {{b.entitled}}</div>
</div>
</div>
<p ng-if="!data.balances.length" class="text-muted">No balance records found.</p>
</div>
Full script — hrlx_balance CSS
.hrlx-balance {
padding: 16px;
h3 { margin-top: 0; font-weight: 600; }
.hrlx-cards {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.hrlx-card {
flex: 1 1 120px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 12px;
text-align: center;
}
.hrlx-card-type { text-transform: capitalize; color: #707070; }
.hrlx-card-remaining { font-size: 28px; font-weight: 700; }
.hrlx-card-meta { font-size: 12px; color: #909090; }
}
Full script — hrlx_balance Client script
/**
* Widget: hrlx_balance — Client Controller
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
*/
function HrlxBalanceCtrl() {
var c = this;
// No client actions needed; data is read-only.
}
Full script — hrlx_balance Server script
/**
* Widget: hrlx_balance — Server Script
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Shows the current user's remaining leave balance for the current year.
*/
(function () {
data.balances = [];
data.year = new GlideDateTime().getYearLocalTime();
// find employee record of current user
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
emp.addQuery('u_user', gs.getUserID());
emp.query();
if (!emp.next()) return;
var bal = new GlideRecordSecure('x_786686_hr_leave_bal_v2');
bal.addQuery('u_employee_ref', emp.getUniqueValue());
bal.addQuery('u_year', data.year);
bal.query();
while (bal.next()) {
data.balances.push({
type: bal.getDisplayValue('u_leave_type'),
entitled: parseInt(bal.getValue('u_entitled') || 0, 10),
used: parseInt(bal.getValue('u_used') || 0, 10),
remaining: parseInt(bal.getValue('u_remaining') || 0, 10)
});
}
})();
Verify checklist
- employee1 sees own requests in widget table.
- Balance cards show entitled/used/remaining for current year.
- Cancel button works on draft/submitted (server update).
Demo / test
Login employee1 → {BASE}/hrlx?id=hrlx_home → requests + balance visible.
Part 29: UI Builder Workspace
Purpose
Now Experience workspace with list page bound to req_v2 for employees and managers.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2.
- Roles Roles.
Form fields — Create workspace
| Field | Value |
|---|---|
| Name | HR Leave Workspace |
| Application | HR Leave |
| Experience type | Workspace / Custom |
Step-by-step create
- UI Builder → Create experience → HR Leave Workspace, scoped app HR Leave.
- Page 1 — My Requests: Add page → Name My Requests → Route my-requests.
- Drag List component → Data resource = new Table data broker.
- Data broker fields: Table = x_786686_hr_leave_req_v2, Condition =
u_requested_for_user=javascript:gs.getUserID()(or UX filter for current user). - Columns: Number, Leave Type, Start, End, Days, State.
- Page 2 — Team Approvals (manager): List on req_v2, filter u_state=submitted AND u_approver_user=current user.
- Page 3 — Admin: Lists for employee_v2 and bal_v2 — role hr_admin only.
- Navigation: Add menu items linking pages. Role restrictions per page.
- Publish workspace → assign to roles employee, manager, hr_admin.
- Open workspace URL from UI Builder preview as each test user.
Verify checklist
- employee sees only own requests on My Requests page.
- manager sees submitted items awaiting approval.
- hr_admin sees admin lists.
Demo / test
Impersonate each role → open workspace → list data matches ACL expectations.
Part 30: Reports & Dashboard
Purpose
Visual report grouping leave requests by state; add to HR Leave dashboard.
Navigation path
Direct URL
Prerequisites
- Part 3: Table req_v2 with seeded mixed states.
Form fields — Report wizard (each screen)
| Wizard step | Selection |
|---|---|
| 1 Source type | Table |
| 2 Table | Leave Request [x_786686_hr_leave_req_v2] |
| 3 Report type | Bar chart or Pie chart |
| 4 Configure | Group by: State (u_state), Aggregate: Count |
| 5 Title | Leave Requests by State |
| Application | HR Leave |
Step-by-step create
- Reports → Create New → Table → req_v2.
- Choose Bar or Pie → Group by u_state → Count.
- Title: Leave Requests by State. Application HR Leave.
- Run report → verify segments for draft/submitted/approved.
- Dashboard: Dashboards → New → Name HR Leave Dashboard.
- Add widget → Report → select report created above.
- Share with roles hr_admin, manager. Save dashboard.
Verify checklist
- Report renders with correct counts.
- Dashboard widget displays report.
Demo / test
Seed 2 draft, 3 submitted, 1 approved → chart reflects counts.
Part 31: ATF Tests
Purpose
Automated Test Framework suite validating BR1 days calc, submit event, audit on approve, and ACL isolation.
Navigation path
Direct URL
Prerequisites
- Part 1: Application Setup
- ATF plugin active.
Form fields — Test Suite
| Field | Value |
|---|---|
| Name | HRLX Leave Core |
| Application | HR Leave |
| Active | true |
Step-by-step create
Test 1 — BR1 business days (Server)
- ATF → Tests → New → Name: HRLX BR1 Days Calculation.
- Step 1: Create a Record → Table req_v2 → set u_start_date=2026-06-01, u_end_date=2026-06-05, u_state=draft, required refs.
- Step 2: Run Server Side Script → assertion:
(function(outputs, steps, stepResult, assertEqual) {
var gr = new GlideRecord('x_786686_hr_leave_req_v2');
gr.get(steps('Create a Record').record_id);
assertEqual({ name: 'days', expected: '5', actual: gr.getValue('u_days_count') });
})(outputs, steps, stepResult, assertEqual);
Test 2 — Client CS2 (Client)
- New test: HRLX CS2 Form Days.
- Step: Open an Existing Record or New Record form on req_v2.
- Step: Set Field Values u_start_date, u_end_date.
- Step: Assert Field Value u_days_count equals 5.
Test 3 — ACL read isolation (Server)
- Test: HRLX ACL Employee Isolation.
- Step: Impersonate user employee1 → Record Query req_v2 → assert count matches only own records.
Suite assembly
- Test Suite → New → HRLX Leave Core.
- Related list Tests → Add all three tests.
- Run Suite → all green.
Verify checklist
- Suite runs without failure on clean seed data.
- Failed steps show actionable BR/ACL errors.
Demo / test
Run HRLX Leave Core suite after E2E seed → 3/3 pass.
Part 32: Scripted REST API
Purpose
Inbound REST POST /leave creates submitted req_v2 from JSON for integration user role.
Navigation path
Direct URL
/api/x_786686_hr_leave/hrlx_api_v2/leavePrerequisites
- Part 16: Roles integration role + user hrlx.integration with rest_service.
- Part 19: ACLs ACL create allows integration role.
- Script Include HRLXProIntegrationUtilsV2 (create below).
- employee_v2 with u_employee_id = EMP001.
Form fields — Script Include HRLXProIntegrationUtilsV2
| Field | Value |
|---|---|
| Name | HRLXProIntegrationUtilsV2 |
| Client callable | false |
| Active | true |
Full script — HRLXProIntegrationUtilsV2
/**
* Script Include: HRLXProIntegrationUtilsV2
* Scope: x_786686_hr_leave
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Client callable: false
* Purpose: Inbound create + Outbound push for leave integration.
*/
var HRLXProIntegrationUtilsV2 = Class.create();
HRLXProIntegrationUtilsV2.prototype = {
initialize: function () {},
/**
* Inbound: create a leave request from a JSON payload (string).
* @return {string} sys_id of created record
*/
inboundCreateLeave: function (jsonString) {
var data = {};
try { data = JSON.parse(jsonString); } catch (e) { return ''; }
var gr = new GlideRecord('x_786686_hr_leave_req_v2');
gr.initialize();
gr.u_employee_ref = this._findEmployeeByExternalId(data.employee_id);
gr.u_requested_for_user = this._findUserByEmployee(data.employee_id);
gr.u_leave_type = data.leave_type || 'annual';
gr.u_start_date = data.start_date || '';
gr.u_end_date = data.end_date || '';
gr.u_reason = data.reason || '';
gr.u_state = 'submitted';
return gr.insert();
},
/**
* Outbound: push an approved leave decision to ERP/Payroll.
* Endpoint + auth come from system properties (no secrets in code).
*/
outboundPushDecision: function (leaveSysId) {
var gr = new GlideRecord('x_786686_hr_leave_req_v2');
if (!gr.get(leaveSysId)) return false;
var endpoint = gs.getProperty('x_786686_hr_leave.config.erp_endpoint');
if (!endpoint) {
gs.error('[HRLX] Missing property x_786686_hr_leave.config.erp_endpoint');
return false;
}
try {
var r = new sn_ws.RESTMessageV2();
r.setEndpoint(endpoint);
r.setHttpMethod('POST');
r.setRequestHeader('Content-Type', 'application/json');
r.setRequestBody(JSON.stringify({
number: gr.getValue('u_number'),
employee: gr.getDisplayValue('u_employee_ref'),
type: gr.getValue('u_leave_type'),
days: gr.getValue('u_days_count'),
state: gr.getValue('u_state')
}));
var resp = r.execute();
var code = resp.getStatusCode();
gs.info('[HRLX] Outbound status ' + code);
return (code >= 200 && code < 300);
} catch (ex) {
gs.error('[HRLX] Outbound error: ' + ex.message);
return false;
}
},
_findEmployeeByExternalId: function (employeeId) {
var e = new GlideRecord('x_786686_hr_leave_employee_v2');
e.addQuery('u_employee_id', employeeId);
e.query();
return e.next() ? e.getUniqueValue() : '';
},
_findUserByEmployee: function (employeeId) {
var e = new GlideRecord('x_786686_hr_leave_employee_v2');
e.addQuery('u_employee_id', employeeId);
e.query();
return e.next() ? e.getValue('u_user') : '';
},
type: 'HRLXProIntegrationUtilsV2'
};
Form fields — Scripted REST API (sys_ws_definition)
| Field | Value |
|---|---|
| Name | HRLX Leave API |
| API ID | hrlx_api_v2 |
| Application | HR Leave (NOT Global) |
| API namespace | x_786686_hr_leave (read-only) |
| Requires authentication | true |
| Default ACLs | ACL execute requiring x_786686_hr_leave.integration |
Form fields — Resource POST /leave
| Field | Value |
|---|---|
| Name | Create Leave |
| HTTP method | POST |
| Relative path | /leave |
| Active | true |
Full script — Resource (POST /leave)
/**
* Application: Custom · Scope: x_786686_hr_leave · sys_id: 83d5298c932b8f107520703efaba1072
* Scripted REST API Resource: Create Leave (Inbound)
* API ID: hrlx_api_v2
* Resource: POST /leave
* Full path: /api/x_786686_hr_leave/hrlx_api_v2/leave
*
* TEST FIRST (no Postman until this works):
* All > System Web Services > REST API Explorer ($restapi.do)
* Namespace: x_786686_hr_leave | API: hrlx_api_v2 | Resource: leave | POST
* Request body: { "employee_id":"EMP001", "leave_type":"annual", ... }
* Click Send → expect 201 → Copy as cURL
*
* Simpler alternative (Table API, no script):
* Explorer → namespace now → Table API → x_786686_hr_leave_req_v2 → POST
* See: src/server/rest/rest_api_explorer.md
*
* Create API record:
* All > System Web Services > Scripted REST APIs > New + Resource POST /leave
*/
(function process(request, response) {
var data = request.body && request.body.data ? request.body.data : {};
// Validate
if (!data.employee_id) {
response.setStatus(400);
return response.setBody({ error: 'employee_id is required' });
}
if (!data.start_date || !data.end_date) {
response.setStatus(400);
return response.setBody({ error: 'start_date and end_date are required' });
}
// Create via integration utility
var sysId = new x_786686_hr_leave.HRLXProIntegrationUtilsV2().inboundCreateLeave(JSON.stringify(data));
if (!sysId) {
response.setStatus(422);
return response.setBody({ error: 'Could not create leave request (employee not found?)' });
}
response.setStatus(201);
response.setBody({ sys_id: sysId, message: 'Leave request created' });
})(request, response);
Step-by-step create
- Application = HR Leave.
- Studio → Script Include → HRLXProIntegrationUtilsV2 → paste script → Save.
- System Security → ACL → New: Type scripted_rest_api, Operation execute, Requires role integration → Save.
- Scripted REST APIs → New → fill API fields → Security tab → Default ACLs → add ACL → Submit.
- Related Resources → New → POST /leave → paste resource script → Submit.
- REST API Explorer → Namespace x_786686_hr_leave → API hrlx_api_v2 → Resource leave → POST.
- Body: {"employee_id":"EMP001","leave_type":"annual","start_date":"2026-06-01","end_date":"2026-06-05","reason":"Vacation"}
- Run as hrlx.integration (Impersonate) → Send → expect 201 + sys_id.
Verify checklist
- 201 response with sys_id.
- req_v2 row exists in submitted state.
- 403 without integration role; 422 if EMP001 missing.
Demo / test
Explorer Send → copy cURL → verify record in req_v2 list.
Chapter map 01–16
| Chapter | Doc HTML | What you create (Studio) | OBJ sections |
|---|---|---|---|
| 01 Development | 01_DEVELOPMENT | App, tables, Script Includes, Business Rules, Client Scripts | 01–13 |
| 02 Administration | 02_ADMIN | Roles, groups, users, ACLs, system property | 16–20 |
| 03 Service Catalog | 03_CATALOG | Record Producer + variables | 26 |
| 04 Service Portal | 04_PORTAL | Portal page + two widgets (HTML/CSS/client/server) | 28 |
| 05 Workspace / UI Builder | 05_UIB | Experience, pages, data resources, list on req_v2 | 29 |
| 06 Notifications & Jobs | 06_NOTIF | Events, email notifications, scheduled accrual job | 21–23 |
| 07 Reports & Dashboards | 07_REPORTS | Report wizard + dashboard widget | 30 |
| 08 ATF Testing | 08_ATF | Test suite + server/client/security steps | 31 |
| 09 Modules & Flow | 09_MODULES | Application menu, modules, approval flow | 24–27 |
| 10 Implementation | 10_IMPL | Seed data, E2E demo, update set completion | E2E |
| 11 Studio practice | 11 practice | Practice: create every section on this page from scratch in order | All sections |
| 12 Integration | 12_INT | Scripted REST API + integration Script Include | 32 |
| 13 Summary | 13_SUMMARY | Review OBJ checklist + E2E | E2E |
| 14 AI Agents | 14_AI | Conceptual — artifacts still created via Studio OBJ list | OBJ catalog |
| 15 Scripts reference | 15_SCRIPTS | Paste scripts: copy embedded <pre> blocks from Script Include HRLXProLeaveUtilsV2..13, 23, 26, 28, 32 | 06+ |
| 16 Full guide | This page | Complete Studio-only create guide for the entire app | Part 1: Application Setup |
End-to-end demo checklist
Complete after Application Setup through Flow Leave Approval (extended: Service Portal–32) to prove the full leave lifecycle.
- Update Set current — HR Leave – Build captures all changes (Rule 0).
- Seed employee — employee_v2 for employee1, manager = manager1 (Part 2: Table, Part 18: Users (lab matrix)).
- Seed balance — bal_v2 year=2026, annual, entitled=20, used=0, remaining=20 (Part 4: Table).
- Create request — req_v2 draft OR catalog Record Producer (Part 3: Table, Part 26: Catalog Record Producer).
- Submit — UI Action or state=submitted → BR2 event (Part 15: UI Action, Part 9: Business Rule BR2).
- Flow approval — manager1 approves → u_state=approved (Part 27: Flow Designer).
- Audit — audit_v2 row submitted→approved (Part 10: Business Rule BR3).
- Balance — bal_v2 u_used up, u_remaining down (Part 10: Business Rule BR3).
- Notification — email/log for approved event (Part 22: Notifications).
- Security — employee1 cannot read another user's request (Part 19: ACLs).
- Portal — employee sees my leaves + balance widgets (Part 28: Service Portal Widgets & Page).
- REST — POST /leave returns 201 as integration user (Part 32: Scripted REST API).
Background seed script
Run in Scripts – Background as admin with HR Leave selected. Replace user sys_ids.
(function () {
var empUser = 'PASTE_EMPLOYEE1_SYS_ID';
var mgrUser = 'PASTE_MANAGER1_SYS_ID';
var emp = new GlideRecord('x_786686_hr_leave_employee_v2');
emp.initialize();
emp.u_user = empUser;
emp.u_employee_id = 'E-1001';
emp.u_department = 'People Ops';
emp.u_manager = mgrUser;
emp.u_join_date = '2024-01-15';
emp.u_grade = 'G5';
var empId = emp.insert();
var bal = new GlideRecord('x_786686_hr_leave_bal_v2');
bal.initialize();
bal.u_employee_ref = empId;
bal.u_year = 2026;
bal.u_leave_type = 'annual';
bal.u_entitled = parseInt(gs.getProperty('x_786686_hr_leave.config.annual_days', 20), 10);
bal.u_used = 0;
bal.u_remaining = bal.u_entitled;
bal.insert();
gs.info('Seeded employee=' + empId);
})();
Quick reference — table names
| employee_v2 | x_786686_hr_leave_employee_v2 |
| req_v2 | x_786686_hr_leave_req_v2 |
| bal_v2 | x_786686_hr_leave_bal_v2 |
| audit_v2 | x_786686_hr_leave_audit_v2 |