HR Leave · Studio Practical

Studio Create All Objects — Docs 01–16 practical

Complete Studio-only walkthrough: create every HR Leave object from scratch on the instance using Studio, classic UI, Flow Designer, Service Catalog, Portal, UI Builder, Reports, ATF, and Scripted REST. No SDK, no Fluent, no CLI, no npx.

Scopex_786686_hr_leave
App sys_ide5f5bc9493a303507520703efaba104d
Instance exampledev398980
URL token{BASE}
Quick open Studio Application Update Sets Flow Designer Service Portal All properties + direct links

From scratch: Create Application Setup through Scripted REST in order. Every section has Prerequisites, Form fields, Step-by-step, Full script (when needed), Verify, and Demo — do not skip.

Professional values & every property: Keep PROFESSIONAL_HR_CREATE_PROPERTIES_AND_LINKS.html open beside this page — master direct links (list + New) and suggested values for Application, tables/columns/choices, SI, BR, CS, UI, roles, ACLs, all system properties, events, notifications, menu/modules, catalog, Flow, portal, reports, ATF, REST, and seed data.

Studio-only rule: Every step uses ServiceNow UI only. Full paste-ready scripts are embedded on this page — copy each <pre> block into the matching Studio record after you create it.

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.

  1. Application picker — top-right globe / application menu → select HR Leave (x_786686_hr_leave). Never leave Global selected when creating scoped artifacts.
  2. Update Set — All → System Update Sets → Local Update Sets → New:
    FieldValue
    NameHR Leave – Build
    ApplicationHR Leave
    StateIn Progress
    DescriptionCaptures all HR Leave lab artifacts
    After submit: open the record → click Make Current. Direct list: {BASE}/sys_update_set_list.do
  3. 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

Form fields

FieldValue
NameHR Leave
Scope namex_786686_hr_leave
Version1.0.0
Short descriptionHR Leave management v2 lab app
Package typeCustom application
Create application menuChecked
StudioChecked (default)

Step-by-step create

  1. Open Studio.
  2. Click Create Application.
  3. Enter every field from the table. Scope must be x_786686_hr_leave.
  4. Click Create. Studio opens HR Leave workspace.
  5. Classic verify: Application record → Scope = x_786686_hr_leave.
  6. Proceed to Rule 0 Update Set → HR Leave – Build → Make Current.

Verify checklist

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

Table form fields (create the table first)

Field on New Table formSuggested professional value
LabelHR Employee
Nameemployee_v2 → full name becomes x_786686_hr_leave_employee_v2
Extends table(leave empty — do not extend Task)
Create moduletrue (or add later in Modules)
Create access controlstrue (refine later in Administration)
ApplicationHR 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

ColumnColumn labelTypeMax lengthReferenceChoice valuesDisplayMandatory
u_userUserReference—sys_user—NoYes
u_employee_idEmployee IDString40——NoYes
u_departmentDepartmentString100——NoNo
u_managerManagerReference—sys_user—NoYes
u_join_dateJoin DateDate———NoNo
u_gradeGradeString20——NoNo

Form Layout — x_786686_hr_leave_employee_v2

  1. All → System Definition → Tables → open x_786686_hr_leave_employee_v2 (or Studio → Tables).
  2. Related link Form Layout → New or edit default.
  3. Drag fields into sections: Profile (u_user, u_employee_id, u_department, u_grade, u_join_date), Management (u_manager).
  4. Save.

List Layout — x_786686_hr_leave_employee_v2

  1. Same table record → related link List Layout.
  2. Add columns (left to right): User, Employee ID, Department, Manager, Join Date, Grade.
  3. Save → open list URL and confirm columns appear.

Step-by-step create

  1. Studio → Create Application File → Table.
  2. Label: HR Employee · Name: employee_v2 → full name x_786686_hr_leave_employee_v2.
  3. Extends table: leave blank (standalone).
  4. Submit table record.
  5. 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.
  6. Complete Form Layout and List Layout steps above.

Verify checklist

Demo / test

  1. New record → User = employee1, Employee ID = E-1001, Manager = manager1.
  2. 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

Table form fields (create the table first)

Field on New Table formSuggested professional value
LabelLeave Request
Namereq_v2 → x_786686_hr_leave_req_v2
Extends table(empty)
Create moduletrue
ApplicationHR 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

ColumnColumn labelTypeMax lengthReferenceChoice valuesDisplayMandatory
u_numberNumberString40——Yes (display)No
u_requested_for_userRequested ForReference—sys_user—NoYes
u_employee_refEmployeeReference—x_786686_hr_leave_employee_v2—NoYes
u_leave_typeLeave TypeChoice——annual, sick, unpaidNoYes
u_start_dateStart DateDate———NoYes
u_end_dateEnd DateDate———NoYes
u_days_countDaysInteger———NoNo (read-only on form)
u_stateStateChoice——draft, submitted, approved, rejected, cancelledNoYes
u_approver_userApproverReference—sys_user—NoNo (set by BR1)
u_reasonReasonString1000——NoConditional (UI Policy sick)

Choice values — u_leave_type

ValueLabel
annualAnnual
sickSick
unpaidUnpaid

Choice values — u_state

ValueLabel
draftDraft
submittedSubmitted
approvedApproved
rejectedRejected
cancelledCancelled

Form Layout — x_786686_hr_leave_req_v2

  1. All → System Definition → Tables → open x_786686_hr_leave_req_v2 (or Studio → Tables).
  2. Related link Form Layout → New or edit default.
  3. 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).
  4. Save.

List Layout — x_786686_hr_leave_req_v2

  1. Same table record → related link List Layout.
  2. Add columns (left to right): Number, Requested For, Leave Type, Start Date, End Date, Days, State, Approver.
  3. Save → open list URL and confirm columns appear.

Step-by-step create

  1. Studio → Create Application File → Table.
  2. Label: Leave Request · Name: req_v2 → x_786686_hr_leave_req_v2.
  3. Create all columns per field table. For Choice columns add values in Choice List related list.
  4. Table record → Display field = u_number.
  5. Dictionary: mark u_days_count read-only on form (optional dictionary flag).
  6. Complete Form Layout and List Layout above.

Verify checklist

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

Table form fields (create the table first)

Field on New Table formSuggested professional value
LabelLeave Balance
Namebal_v2 → x_786686_hr_leave_bal_v2
Extends table(empty)
ApplicationHR 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

ColumnColumn labelTypeMax lengthReferenceChoice valuesDisplayMandatory
u_employee_refEmployeeReference—x_786686_hr_leave_employee_v2—NoYes
u_yearYearInteger———NoYes
u_leave_typeLeave TypeChoice——annual, sick, unpaidNoYes
u_entitledEntitledInteger———NoYes
u_usedUsedInteger———NoNo
u_remainingRemainingInteger———NoNo

Form Layout — x_786686_hr_leave_bal_v2

  1. All → System Definition → Tables → open x_786686_hr_leave_bal_v2 (or Studio → Tables).
  2. Related link Form Layout → New or edit default.
  3. Drag fields into sections: Employee (u_employee_ref, u_year, u_leave_type), Balance (u_entitled, u_used, u_remaining).
  4. Save.

List Layout — x_786686_hr_leave_bal_v2

  1. Same table record → related link List Layout.
  2. Add columns (left to right): Employee, Year, Leave Type, Entitled, Used, Remaining.
  3. Save → open list URL and confirm columns appear.

Step-by-step create

  1. Studio → Table → Name bal_v2.
  2. Add six columns per table.
  3. Reuse u_leave_type choice values from req_v2.
  4. Form/List layouts per steps above.

Verify checklist

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

Table form fields (create the table first)

Field on New Table formSuggested professional value
LabelLeave Audit
Nameaudit_v2 → x_786686_hr_leave_audit_v2
Extends table(empty)
ApplicationHR 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

ColumnColumn labelTypeMax lengthReferenceChoice valuesDisplayMandatory
u_leave_refLeaveReference—x_786686_hr_leave_req_v2—NoYes
u_old_stateOld StateString40——NoNo
u_new_stateNew StateString40——NoYes
u_event_typeEvent TypeString60——NoNo
u_actorActorReference—sys_user—NoNo

Form Layout — x_786686_hr_leave_audit_v2

  1. All → System Definition → Tables → open x_786686_hr_leave_audit_v2 (or Studio → Tables).
  2. Related link Form Layout → New or edit default.
  3. Drag fields into sections: Audit (u_leave_ref, u_event_type, u_old_state, u_new_state, u_actor).
  4. Save.

List Layout — x_786686_hr_leave_audit_v2

  1. Same table record → related link List Layout.
  2. Add columns (left to right): Leave Request, Old State, New State, Event Type, Actor, Created.
  3. Save → open list URL and confirm columns appear.

Step-by-step create

  1. Studio → Table → audit_v2.
  2. Add five columns.
  3. Layouts as above.

Verify checklist

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

Form fields

FieldValue
NameHRLXProLeaveUtilsV2
API Namex_786686_hr_leave.HRLXProLeaveUtilsV2
Client callableChecked
ActiveChecked
Accessible fromAll application scopes
ScriptPaste block below

Extends AbstractAjaxProcessor for ajaxGetBusinessDays.

Step-by-step create

  1. Studio → Script Include → New.
  2. Fill form fields table.
  3. Paste full script below into Script field.
  4. 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

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

Form fields

FieldValue
NameHRLXProSecurityUtilsV2
Client callableUnchecked
ActiveChecked

Step-by-step create

  1. Studio → Script Include → New.
  2. Name = HRLXProSecurityUtilsV2, Client callable = false.
  3. 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

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

Form fields

FieldValue
NameHRLX BR1 Calculate Days
Tablex_786686_hr_leave_req_v2
AdvancedChecked
Whenbefore
InsertChecked
UpdateChecked
DeleteUnchecked
Order100
ActiveChecked

Step-by-step create

  1. Studio → Business Rule → New.
  2. Fill every field from table. Check Advanced.
  3. Paste script below into Script field.
  4. 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

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

Form fields

FieldValue
NameHRLX BR2 On Submit Event
Tablex_786686_hr_leave_req_v2
AdvancedChecked
Whenafter
InsertChecked
UpdateChecked
DeleteUnchecked
Order200
ActiveChecked

Step-by-step create

  1. Studio → Business Rule → New.
  2. Fill every field from table. Check Advanced.
  3. Paste script below into Script field.
  4. 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

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

Form fields

FieldValue
NameHRLX BR3 Audit State Change
Tablex_786686_hr_leave_req_v2
AdvancedChecked
Whenafter
InsertUnchecked
UpdateChecked
DeleteUnchecked
Order300
ActiveChecked

Step-by-step create

  1. Studio → Business Rule → New.
  2. Fill every field from table. Check Advanced.
  3. Paste script below into Script field.
  4. 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

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

Form fields

FieldValue
NameHRLX BR4 Approve Reject Event
Tablex_786686_hr_leave_req_v2
AdvancedChecked
Whenafter
InsertUnchecked
UpdateChecked
DeleteUnchecked
Order250
ActiveChecked

Step-by-step create

  1. Studio → Business Rule → New.
  2. Fill every field from table. Check Advanced.
  3. Paste script below into Script field.
  4. 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

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

Form fields

FieldValue
NameHRLX CS1 Validate Dates
Tablex_786686_hr_leave_req_v2
TypeonChange
Field nameu_end_date
UI TypeDesktop + All checked
ActiveChecked

Step-by-step create

  1. Studio → Client Script → New.
  2. Fill fields. Type = onChange, Field = u_end_date.
  3. 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

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

Form fields

FieldValue
NameHRLX CS2 Calc Days GlideAjax
Tablex_786686_hr_leave_req_v2
TypeonChange
Field nameu_end_date (duplicate CS for u_start_date if desired)
UI TypeDesktop + All
ActiveChecked

Step-by-step create

  1. Create Client Script after Script Include HRLXProLeaveUtilsV2.
  2. onChange on u_end_date — same field as CS1 (both execute).
  3. 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

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

Form fields

FieldValue
Tablex_786686_hr_leave_req_v2
Short descriptionSick leave requires reason
ApplicationHR Leave
ActiveChecked
Reverse if falseChecked
On loadChecked
Conditionu_leave_type is sick

UI Policy Action (related list New)

FieldValue
Fieldu_reason
Mandatorytrue
Visibletrue

Step-by-step create

  1. Application = HR Leave.
  2. All → System UI → UI Policies → New.
  3. Fill policy fields. Condition builder: Leave Type is sick.
  4. Submit policy.
  5. Related list UI Policy Actions → New → u_reason mandatory true.
  6. Save action. Verify policy Active.

Verify checklist

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

Form fields

FieldValue
NameSubmit
Tablex_786686_hr_leave_req_v2
Action namesubmit_leave
Form buttonChecked
Form stylePrimary (optional)
Order100
Conditioncurrent.u_state == 'draft'
ClientUnchecked (server script)
ActiveChecked

Step-by-step create

  1. Studio → UI Action → New on req_v2.
  2. Fill fields. Uncheck Client checkbox.
  3. Paste server script below in Script field.
  4. Save. Open draft req_v2 → Submit button visible.

Full script (paste-ready)

current.u_state = 'submitted';
current.update();
action.setRedirectURL(current);

Verify checklist

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

Form fields

NameDescriptionAssignable by
x_786686_hr_leave.employeeSubmit and view own leavehr_admin
x_786686_hr_leave.managerApprove team leavehr_admin
x_786686_hr_leave.hr_adminFull HR Leave adminadmin
x_786686_hr_leave.integrationREST / integration useradmin

Step-by-step create

  1. For each row: Studio → Role → New.
  2. Name = scoped role name (prefix auto). Description as table. Application = HR Leave.
  3. Submit. Repeat four times.

Verify checklist

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

Form fields

Group nameGroup RolesMembers
HRLX Employeesx_786686_hr_leave.employeeemployee1
HRLX Managersx_786686_hr_leave.manager, employeemanager1
HRLX HR Adminsx_786686_hr_leave.hr_adminhradmin
HRLX Integrationx_786686_hr_leave.integration, rest_servicehrlx.integration

Step-by-step create

  1. Groups → New → Name from table.
  2. Related list Group Roles → Edit → add scoped roles.
  3. Related list Users → add members (create users in Users if missing).
  4. Repeat for all four groups.

Verify checklist

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

Form fields

User IDNameEmailGroup
employee1Lab Employee[email protected]HRLX Employees
manager1Lab Manager[email protected]HRLX Managers
hradminLab HR Admin[email protected]HRLX HR Admins
hrlx.integrationIntegration Bot[email protected]HRLX Integration

Step-by-step create

  1. Users → New for each row. Set password per instance policy.
  2. Active = true. Email required for notifications Notifications.
  3. Add to group via Groups related list or group Members list.
  4. Seed employee_v2: employee1 user, manager = manager1, Employee ID E-1001.

Verify checklist

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

Form fields

ACL 1 — read

FieldValue
Typerecord
Operationread
Namex_786686_hr_leave_req_v2
AdvancedChecked
Scriptanswer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canReadLeave(current);

ACL 2 — write

Operationwrite
Scriptanswer = new x_786686_hr_leave.HRLXProSecurityUtilsV2().canWriteLeave(current);

ACL 3 — create

Operationcreate
Requires rolex_786686_hr_leave.employee
Scriptanswer = 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

Namex_786686_hr_leave_req_v2.u_approver_user
Operationwrite
Scriptanswer = gs.hasRole('x_786686_hr_leave.hr_admin');

Step-by-step create

  1. Elevate security_admin. Application = HR Leave.
  2. ACL → New for each table above. Active = true.
  3. Paste scripts. Save all four.

Verify checklist

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

Form fields — create each row (Name / Type / Value / Description)

NameTypeValueDescription
x_786686_hr_leave.config.annual_daysinteger20Default annual entitlement
x_786686_hr_leave.config.sick_daysinteger10Default sick entitlement
x_786686_hr_leave.config.notify_enabledtrue|falsetrueMaster email switch
x_786686_hr_leave.config.min_notice_daysinteger3Min days before start (optional BR)
x_786686_hr_leave.config.max_days_per_requestinteger30Max days per request
x_786686_hr_leave.config.erp_endpointstringhttps://webhook.site/…Outbound ERP URL (no secrets)
x_786686_hr_leave.config.integration_userstringhrlx.integrationInbound API user id
x_786686_hr_leave.config.number_prefixstringHRLXu_number prefix
x_786686_hr_leave.config.portal_page_idstringhrlx_homeService Portal page id

On every property form also set: Application = HR Leave · Ignore cache = false · Private = false.

Step-by-step create

  1. Application picker = HR Leave.
  2. Open New property.
  3. Fill Name, Type, Value, Description from one row → Submit.
  4. Repeat for all 9 rows. Confirm list: filter list.

Verify checklist

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

Form fields

Event nameTableActive
x_786686_hr_leave.leave_v2.submittedx_786686_hr_leave_req_v2true
x_786686_hr_leave.leave_v2.approvedx_786686_hr_leave_req_v2true
x_786686_hr_leave.leave_v2.rejectedx_786686_hr_leave_req_v2true

Step-by-step create

  1. Registry → New (three times).
  2. Event name exactly as table. Table = Leave Request. Application = HR Leave. Active.

Verify checklist

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

Form fields

Notification 1 — Submitted

FieldValue
NameHRLX Leave Submitted
Tablex_786686_hr_leave_req_v2
Activetrue
When to sendEvent is fired
Event namex_786686_hr_leave.leave_v2.submitted
Users/Groups in fieldsu_approver_user
SubjectLeave ${u_number} needs your approval

Notification 2 — Approved

Eventleave_v2.approved
Users/Groups in fieldsu_requested_for_user
SubjectYour leave ${u_number} was approved

Notification 3 — Rejected

Eventleave_v2.rejected
Users/Groups in fieldsu_requested_for_user
SubjectYour leave ${u_number} was rejected

Step-by-step create

  1. Application = HR Leave. Notifications → New for each.
  2. Tab When to send: Event is fired + event name.
  3. Tab Who will receive: Users/Groups in fields → pick u_approver_user or u_requested_for_user.
  4. Tab What it will contain: Subject + Message HTML with ${u_number}, ${u_start_date}, ${URI_REF}.
  5. Submit each notification. Active = true.

Verify checklist

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

Form fields

FieldValue
NameHRLX Annual Accrual
ApplicationHR Leave
ActiveChecked
RunDaily 02:00 or Yearly Jan 1
Run asSystem (default)

Step-by-step create

  1. Scheduled Script Execution → New.
  2. Fill fields. Paste script below.
  3. 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

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

Form fields

FieldValue
TitleHR Leave
ApplicationHR Leave
Order100
Rolesx_786686_hr_leave.employee (minimum)

Step-by-step create

  1. If missing: Studio → Application Menu → New → fill fields.
  2. Save. Confirm appears in Application Navigator.

Verify checklist

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

Form fields

TitleTypeTableOrderRoles
Leave RequestsListx_786686_hr_leave_req_v210employee, manager, hr_admin
EmployeesListx_786686_hr_leave_employee_v220hr_admin
BalancesListx_786686_hr_leave_bal_v230hr_admin
Audit TrailListx_786686_hr_leave_audit_v240hr_admin

Step-by-step create

  1. Module → New for each row. Application Menu = HR Leave.
  2. Type = List → Table = as listed. Order as table.
  3. Roles: restrict Employees/Balances/Audit to hr_admin; Requests to all three roles.

Verify checklist

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

Form fields

FieldValue
NameSubmit Leave Request
Tablex_786686_hr_leave_req_v2
CategoryHR Leave
Rolesx_786686_hr_leave.employee
Activetrue

Variables (related list)

NameTypeQuestionMandatory
leave_typeChoiceLeave TypeYes
start_dateDateStart DateYes
end_dateDateEnd DateYes
reasonMulti Line TextReasonNo

Step-by-step create

  1. Record Producers → New. Fill header fields.
  2. Variables related list → New for each variable (names must match producer.* in script).
  3. Script tab → paste producer script below.
  4. 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

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

Form fields — Create Flow wizard

FieldValue
NameHRLX Leave Approval
ApplicationHR Leave
DescriptionApproval flow for submitted leave requests

Trigger — Record Updated

FieldValue
Trigger typeRecord Updated
Tablex_786686_hr_leave_req_v2
ConditionState [u_state] is submitted AND State changes
Run flowOnce per record (recommended)

Action 1 — Ask For Approval

InputValue
RecordTrigger → Leave Request Record (data pill)
Tablex_786686_hr_leave_req_v2
Approval fieldu_approver_user
Approval reasonLeave approval required
Rules → ApproveAnyone approves (required — do not leave empty)

If Approved → Update Record

RecordTrigger record pill
Field mapu_state = approved

Else → Update Record

Field mapu_state = rejected

Step-by-step create

  1. Flow Designer → Create Flow → Name HRLX Leave Approval, Application HR Leave.
  2. Add trigger → Record Updated → table req_v2 → condition u_state is submitted + changes.
  3. + → Ask for Approval → fill Action 1 table → Rules: Anyone approves → Done.
  4. + → Flow Logic → If → condition "approval was Approved".
  5. Yes branch → Update Record → u_state = approved.
  6. Else branch → Update Record → u_state = rejected.
  7. Save → fix any red errors (missing approval rule is common).
  8. Test → pick sample submitted req_v2 → Run test.
  9. Publish → Publish new version → wait for Published status.

Verify checklist

Demo / test

  1. employee1 submits leave (UI Action or catalog).
  2. manager1 → My Approvals → Approve.
  3. 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

Form fields — Portal (sp_portal) if creating new

FieldValue
TitleHR Leave Portal
URL suffixhrlx
ApplicationHR Leave

Page (sp_page)

FieldValue
IDhrlx_home
TitleHR Leave Home
ApplicationHR Leave

Widget 1 — hrlx_my_leaves

FieldValue
IDhrlx_my_leaves
NameHRLX My Leaves
Client controllerHrlxMyLeavesCtrl (as in client script)

Widget 2 — hrlx_balance

FieldValue
IDhrlx_balance
NameHRLX Balance
Client controllerHrlxBalanceCtrl

Step-by-step create

  1. Portal: Service Portal → Portals → New OR use existing Service Portal → set url_suffix hrlx on dedicated portal record scoped to HR Leave.
  2. Widget hrlx_my_leaves: SP → Widgets → New → ID hrlx_my_leaves → paste HTML, CSS, Client, Server from sections below.
  3. Widget hrlx_balance: Repeat for hrlx_balance.
  4. Page: SP → Pages → New → ID hrlx_home → drag both widgets onto 12-column layout (balance top, my leaves below).
  5. Container/page access: Page → security / roles → x_786686_hr_leave.employee.
  6. Menu (optional): SP → Menus → add link to ?id=hrlx_home.
  7. Save all. Open {BASE}/hrlx?id=hrlx_home as 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

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

Form fields — Create workspace

FieldValue
NameHR Leave Workspace
ApplicationHR Leave
Experience typeWorkspace / Custom

Step-by-step create

  1. UI Builder → Create experience → HR Leave Workspace, scoped app HR Leave.
  2. Page 1 — My Requests: Add page → Name My Requests → Route my-requests.
  3. Drag List component → Data resource = new Table data broker.
  4. Data broker fields: Table = x_786686_hr_leave_req_v2, Condition = u_requested_for_user=javascript:gs.getUserID() (or UX filter for current user).
  5. Columns: Number, Leave Type, Start, End, Days, State.
  6. Page 2 — Team Approvals (manager): List on req_v2, filter u_state=submitted AND u_approver_user=current user.
  7. Page 3 — Admin: Lists for employee_v2 and bal_v2 — role hr_admin only.
  8. Navigation: Add menu items linking pages. Role restrictions per page.
  9. Publish workspace → assign to roles employee, manager, hr_admin.
  10. Open workspace URL from UI Builder preview as each test user.

Verify checklist

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

Form fields — Report wizard (each screen)

Wizard stepSelection
1 Source typeTable
2 TableLeave Request [x_786686_hr_leave_req_v2]
3 Report typeBar chart or Pie chart
4 ConfigureGroup by: State (u_state), Aggregate: Count
5 TitleLeave Requests by State
ApplicationHR Leave

Step-by-step create

  1. Reports → Create New → Table → req_v2.
  2. Choose Bar or Pie → Group by u_state → Count.
  3. Title: Leave Requests by State. Application HR Leave.
  4. Run report → verify segments for draft/submitted/approved.
  5. Dashboard: Dashboards → New → Name HR Leave Dashboard.
  6. Add widget → Report → select report created above.
  7. Share with roles hr_admin, manager. Save dashboard.

Verify checklist

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

Form fields — Test Suite

FieldValue
NameHRLX Leave Core
ApplicationHR Leave
Activetrue

Step-by-step create

Test 1 — BR1 business days (Server)

  1. ATF → Tests → New → Name: HRLX BR1 Days Calculation.
  2. 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.
  3. 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)

  1. New test: HRLX CS2 Form Days.
  2. Step: Open an Existing Record or New Record form on req_v2.
  3. Step: Set Field Values u_start_date, u_end_date.
  4. Step: Assert Field Value u_days_count equals 5.

Test 3 — ACL read isolation (Server)

  1. Test: HRLX ACL Employee Isolation.
  2. Step: Impersonate user employee1 → Record Query req_v2 → assert count matches only own records.

Suite assembly

  1. Test Suite → New → HRLX Leave Core.
  2. Related list Tests → Add all three tests.
  3. Run Suite → all green.

Verify checklist

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

{BASE}/sys_ws_definition_list.do · Explorer: {BASE}/$restapi.do · Endpoint: /api/x_786686_hr_leave/hrlx_api_v2/leave

Prerequisites

Form fields — Script Include HRLXProIntegrationUtilsV2

FieldValue
NameHRLXProIntegrationUtilsV2
Client callablefalse
Activetrue

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)

FieldValue
NameHRLX Leave API
API IDhrlx_api_v2
ApplicationHR Leave (NOT Global)
API namespacex_786686_hr_leave (read-only)
Requires authenticationtrue
Default ACLsACL execute requiring x_786686_hr_leave.integration

Form fields — Resource POST /leave

FieldValue
NameCreate Leave
HTTP methodPOST
Relative path/leave
Activetrue

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

  1. Application = HR Leave.
  2. Studio → Script Include → HRLXProIntegrationUtilsV2 → paste script → Save.
  3. System Security → ACL → New: Type scripted_rest_api, Operation execute, Requires role integration → Save.
  4. Scripted REST APIs → New → fill API fields → Security tab → Default ACLs → add ACL → Submit.
  5. Related Resources → New → POST /leave → paste resource script → Submit.
  6. REST API Explorer → Namespace x_786686_hr_leave → API hrlx_api_v2 → Resource leave → POST.
  7. Body: {"employee_id":"EMP001","leave_type":"annual","start_date":"2026-06-01","end_date":"2026-06-05","reason":"Vacation"}
  8. Run as hrlx.integration (Impersonate) → Send → expect 201 + sys_id.

Verify checklist

Demo / test

Explorer Send → copy cURL → verify record in req_v2 list.


Chapter map 01–16

ChapterDoc HTMLWhat you create (Studio)OBJ sections
01 Development01_DEVELOPMENTApp, tables, Script Includes, Business Rules, Client Scripts01–13
02 Administration02_ADMINRoles, groups, users, ACLs, system property16–20
03 Service Catalog03_CATALOGRecord Producer + variables26
04 Service Portal04_PORTALPortal page + two widgets (HTML/CSS/client/server)28
05 Workspace / UI Builder05_UIBExperience, pages, data resources, list on req_v229
06 Notifications & Jobs06_NOTIFEvents, email notifications, scheduled accrual job21–23
07 Reports & Dashboards07_REPORTSReport wizard + dashboard widget30
08 ATF Testing08_ATFTest suite + server/client/security steps31
09 Modules & Flow09_MODULESApplication menu, modules, approval flow24–27
10 Implementation10_IMPLSeed data, E2E demo, update set completionE2E
11 Studio practice11 practicePractice: create every section on this page from scratch in orderAll sections
12 Integration12_INTScripted REST API + integration Script Include32
13 Summary13_SUMMARYReview OBJ checklist + E2EE2E
14 AI Agents14_AIConceptual — artifacts still created via Studio OBJ listOBJ catalog
15 Scripts reference15_SCRIPTSPaste scripts: copy embedded <pre> blocks from Script Include HRLXProLeaveUtilsV2..13, 23, 26, 28, 3206+
16 Full guideThis pageComplete Studio-only create guide for the entire appPart 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.

  1. Update Set current — HR Leave – Build captures all changes (Rule 0).
  2. Seed employee — employee_v2 for employee1, manager = manager1 (Part 2: Table, Part 18: Users (lab matrix)).
  3. Seed balance — bal_v2 year=2026, annual, entitled=20, used=0, remaining=20 (Part 4: Table).
  4. Create request — req_v2 draft OR catalog Record Producer (Part 3: Table, Part 26: Catalog Record Producer).
  5. Submit — UI Action or state=submitted → BR2 event (Part 15: UI Action, Part 9: Business Rule BR2).
  6. Flow approval — manager1 approves → u_state=approved (Part 27: Flow Designer).
  7. Audit — audit_v2 row submitted→approved (Part 10: Business Rule BR3).
  8. Balance — bal_v2 u_used up, u_remaining down (Part 10: Business Rule BR3).
  9. Notification — email/log for approved event (Part 22: Notifications).
  10. Security — employee1 cannot read another user's request (Part 19: ACLs).
  11. Portal — employee sees my leaves + balance widgets (Part 28: Service Portal Widgets & Page).
  12. 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_v2x_786686_hr_leave_employee_v2
req_v2x_786686_hr_leave_req_v2
bal_v2x_786686_hr_leave_bal_v2
audit_v2x_786686_hr_leave_audit_v2