UI Actions

SkillSecurity

Complete guide to UI Action development including form buttons, list buttons, context menu actions, client-side and server-side scripts, conditions, security, and common patterns

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the UI Actions skill

What this skill tells your AI

The instructions your AI receives, as published by happy-technologies-llc/happy-platform-skills in skills/development/ui-actions/SKILL.md and read by ahel’s review.

Overview

This skill covers creating and configuring UI Actions in ServiceNow:

  • Form buttons (header/footer) and form context menu items
  • List buttons, list context menu, and list choice actions
  • Client-side scripts, server-side scripts, and combined execution
  • Visibility conditions and role-based security
  • Common patterns: Approve, Reject, Clone, Escalate, etc.
  • Testing and debugging UI Actions

When to use: When you need to add custom buttons or menu items to forms and lists that execute client-side or server-side logic.

Who should use this: Developers and administrators who need to extend ServiceNow forms and lists with custom functionality.

Prerequisites

  • Roles: admin, personalize (for basic UI actions), or specific application roles
  • Access: sys_ui_action table, related tables for testing
  • Knowledge: JavaScript, GlideRecord API, client-side g_form API
  • Related Skills: admin/script-execution for server-side patterns

Understanding UI Actions

UI Action Types

┌─────────────────────────────────────────────────────────────────────────────┐
│                           UI Action Locations                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────── FORM ────────────┐    ┌─────────── LIST ────────────┐        │
│  │                             │    │                             │        │
│  │  [Form Button] [Button]     │    │  [List Button] [Button]     │        │
│  │  ─────────────────────────  │    │  ─────────────────────────  │        │
│  │  ┌─────────────────────┐    │    │  ☐ Number    Description    │        │
│  │  │                     │    │    │  ☐ INC001   Server down     │        │
│  │  │   Form Content      │    │    │  ☐ INC002   Email issue     │        │
│  │  │                     │    │    │                             │        │
│  │  │   Right-click →     │    │    │   Right-click →             │        │
│  │  │   [Context Menu]    │    │    │   [Context Menu]            │        │
│  │  │                     │    │    │                             │        │
│  │  └─────────────────────┘    │    │  Actions Dropdown:          │        │
│  │                             │    │   ☐ List Choice Option      │        │
│  │  [Footer Button] [Button]   │    │                             │        │
│  └─────────────────────────────┘    └─────────────────────────────┘        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

UI Action Execution Flow

User Clicks UI Action
        │
        ▼
┌───────────────────┐
│ Check Conditions  │
│ (if defined)      │
└────────┬──────────┘
         │
    Condition
      True?
    ┌───┴───┐
   Yes     No
    │       │
    ▼       ▼
┌───────┐ [Action
│Client │  Hidden]
│Script?│
└───┬───┘
    │
   Yes ──► Run Client Script (onclick)
    │           │
    │       gsftSubmit()?
    │       ┌───┴───┐
    │      Yes     No
    │       │       │
    │       ▼       ▼
    │   ┌───────┐ [Client
    │   │Submit │  Only]
    │   │ Form  │
    │   └───┬───┘
    │       │
    ▼       ▼
┌───────────────────┐
│ Run Server Script │◄── No Client Script (direct server)
└───────────────────┘
         │
         ▼
    ┌─────────┐
    │Redirect?│
    └────┬────┘
     ┌───┴───┐
    Yes     No
     │       │
     ▼       ▼
[Custom   [Form
 URL]     Reload]

Procedure

Phase 1: Understanding UI Action Fields

Step 1.1: Get Table Schema

Query the sys_ui_action schema:

Tool: SN-Get-Table-Schema
Parameters:
  table_name: sys_ui_action
Step 1.2: Key Fields Reference
FieldTypeDescription
nameStringButton/menu item label
tableStringTarget table (e.g., incident)
action_nameStringInternal name (no spaces, used in URLs)
orderIntegerDisplay order (lower = first)
activeBooleanEnable/disable the action
clientBooleanRun client-side script (onclick)
scriptScriptServer-side GlideRecord script
onclickScriptClient-side JavaScript (g_form)
conditionScriptServer-side visibility condition
commentsStringDescription/documentation
Step 1.3: Location Fields
FieldTypeDescription
form_buttonBooleanShow as form header button
form_button_v2BooleanShow as form footer button (v2 forms)
form_linkBooleanShow in form context menu
list_banner_buttonBooleanShow as list header button
list_buttonBooleanShow in list actions (legacy)
list_choiceBooleanShow in list Actions dropdown
list_context_menuBooleanShow in list right-click menu
list_linkBooleanShow in list Related Links

Phase 2: Create Form Button UI Actions

Step 2.1: Basic Form Button

Create a simple form button that runs server-side:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Escalate to Management"
    table: incident
    action_name: escalate_to_management
    order: 100
    active: true
    form_button: true
    client: false
    script: |
      // Server-side script
      current.escalation = 1;
      current.priority = 1;
      current.work_notes = "Escalated to management by " + gs.getUserDisplayName();
      current.update();
      action.setRedirectURL(current);
    comments: "Escalates incident to management with P1 priority"

Result: Returns sys_id of created UI action.

Step 2.2: Form Button with Client Script

Create a button with client-side confirmation:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Close Incident"
    table: incident
    action_name: close_incident
    order: 200
    active: true
    form_button: true
    client: true
    onclick: |
      // Client-side script
      var closeCode = g_form.getValue('close_code');
      var closeNotes = g_form.getValue('close_notes');

      // Validate required fields
      if (!closeCode) {
        g_form.showFieldMsg('close_code', 'Close code is required', 'error');
        return false;  // Prevent submission
      }

      if (!closeNotes) {
        g_form.showFieldMsg('close_notes', 'Close notes are required', 'error');
        return false;
      }

      // Confirm with user
      if (!confirm('Are you sure you want to close this incident?')) {
        return false;
      }

      // Submit the form
      gsftSubmit(null, g_form.getFormElement(), 'close_incident');
    script: |
      // Server-side script (runs after form submit)
      current.state = 7;  // Closed
      current.closed_at = new GlideDateTime();
      current.closed_by = gs.getUserID();
      current.update();
      action.setRedirectURL(current);
    comments: "Closes incident with validation and confirmation"
Step 2.3: Form Header vs Footer Button

Form Header Button (form_button):

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Quick Assign"
    table: incident
    action_name: quick_assign
    order: 50
    active: true
    form_button: true
    form_button_v2: false
    # ... rest of configuration

Form Footer Button (form_button_v2):

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Save and Return"
    table: incident
    action_name: save_and_return
    order: 300
    active: true
    form_button: false
    form_button_v2: true
    # ... rest of configuration

Phase 3: Create List UI Actions

Step 3.1: List Banner Button

Button that appears at the top of lists:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Export Selected"
    table: incident
    action_name: export_selected
    order: 100
    active: true
    list_banner_button: true
    client: true
    onclick: |
      // Get selected records
      var list = GlideList2.get(g_form.getTableName());
      var checked = list.getChecked();

      if (!checked) {
        alert('Please select at least one record');
        return false;
      }

      // Redirect to export with selected sys_ids
      var url = 'export_selected.do?sysparm_sys_ids=' + checked;
      window.location = url;
    comments: "Exports selected incidents to CSV"
Step 3.2: List Context Menu

Right-click menu item for list rows:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Assign to Me"
    table: incident
    action_name: assign_to_me_list
    order: 100
    active: true
    list_context_menu: true
    client: false
    script: |
      // Server-side script
      // current is the right-clicked record
      current.assigned_to = gs.getUserID();
      current.assignment_group = gs.getUser().getMyGroups()[0];
      current.work_notes = "Self-assigned via list context menu";
      current.update();
    comments: "Assigns the right-clicked incident to current user"
Step 3.3: List Choice (Actions Dropdown)

Bulk action in the Actions dropdown:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Bulk Close"
    table: incident
    action_name: bulk_close
    order: 200
    active: true
    list_choice: true
    client: true
    onclick: |
      // Get checked records
      var list = GlideList2.get(g_form.getTableName());
      var checked = list.getChecked();

      if (!checked) {
        alert('Please select at least one incident to close');
        return false;
      }

      var count = checked.split(',').length;
      if (!confirm('Close ' + count + ' selected incident(s)?')) {
        return false;
      }

      // Submit to server
      gsftSubmit(null, g_form.getFormElement(), 'bulk_close');
    script: |
      // Server-side - process all checked records
      var sysIds = RP.getParameterValue('sysparm_checked_items');
      if (sysIds) {
        var ids = sysIds.split(',');
        for (var i = 0; i < ids.length; i++) {
          var gr = new GlideRecord('incident');
          if (gr.get(ids[i])) {
            gr.state = 7;  // Closed
            gr.close_code = 'Solved (Permanently)';
            gr.close_notes = 'Bulk closed by ' + gs.getUserDisplayName();
            gr.update();
          }
        }
      }
    comments: "Closes all selected incidents in bulk"

Phase 4: Context Menu (Form Right-Click)

Step 4.1: Form Context Menu Item

Add item to form right-click menu:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "View Audit History"
    table: incident
    action_name: view_audit_history
    order: 500
    active: true
    form_link: true
    client: true
    onclick: |
      // Open audit history in new window
      var sysId = g_form.getUniqueValue();
      var tableName = g_form.getTableName();
      var url = 'sys_audit_list.do?sysparm_query=tablename=' + tableName +
                '^documentkey=' + sysId + '&sysparm_view=';
      window.open(url, '_blank');
    comments: "Opens audit history for current record in new tab"
Step 4.2: Form Link with Server Script

Context menu that runs server-side:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Create Child Incident"
    table: incident
    action_name: create_child_incident
    order: 600
    active: true
    form_link: true
    client: false
    script: |
      // Create child incident
      var child = new GlideRecord('incident');
      child.initialize();
      child.parent_incident = current.sys_id;
      child.short_description = 'Child of: ' + current.short_description;
      child.description = 'Child incident created from ' + current.number;
      child.caller_id = current.caller_id;
      child.category = current.category;
      child.subcategory = current.subcategory;
      child.priority = current.priority;
      var childId = child.insert();

      // Redirect to new child incident
      action.setRedirectURL(child);
    comments: "Creates a child incident linked to current record"

Phase 5: Conditions for Visibility

Step 5.1: Simple Field Conditions

Show button only for specific states:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Reopen Incident"
    table: incident
    action_name: reopen_incident
    order: 100
    active: true
    form_button: true
    condition: "current.state == 7"
    script: |
      // Reopen logic
      current.state = 2;  // In Progress
      current.work_notes = "Reopened by " + gs.getUserDisplayName();
      current.update();
      action.setRedirectURL(current);
    comments: "Visible only when incident is Closed (state=7)"

Common Condition Patterns:

ConditionMeaning
current.state == 1State equals New
current.active == trueRecord is active
current.assigned_to == gs.getUserID()Assigned to current user
current.assignment_group.manager == gs.getUserID()Current user is group manager
current.isNewRecord()New record (not saved yet)
!current.isNewRecord()Existing record only
current.priority <= 2Priority is 1 or 2
current.caller_id.vip == trueCaller is VIP
Step 5.2: Role-Based Conditions

Show button only for specific roles:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Override SLA"
    table: incident
    action_name: override_sla
    order: 100
    active: true
    form_button: true
    condition: "gs.hasRole('incident_manager') || gs.hasRole('admin')"
    script: |
      // SLA override logic
      current.sla_due = '';
      current.work_notes = "SLA overridden by manager";
      current.update();
    comments: "Visible only to incident managers and admins"
Step 5.3: Complex Conditions

Multiple conditions combined:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Emergency Escalation"
    table: incident
    action_name: emergency_escalation
    order: 50
    active: true
    form_button: true
    condition: |
      // Only show for P1/P2 active incidents assigned to current user's group
      current.state != 6 && current.state != 7 &&
      current.priority <= 2 &&
      current.assignment_group.getDisplayValue() != '' &&
      gs.getUser().isMemberOf(current.assignment_group)
    script: |
      // Emergency escalation logic
      current.escalation = 3;
      current.work_notes = "[EMERGENCY ESCALATION] " + gs.getUserDisplayName();
      current.update();
    comments: "Emergency escalation for P1/P2 incidents in user's group"
Step 5.4: UI Action Visibility Best Practices
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Approve Request"
    table: sc_req_item
    action_name: approve_request
    order: 100
    active: true
    form_button: true
    condition: |
      // Best practice: Check multiple conditions
      var canApprove = false;

      // 1. Check record state
      if (current.approval == 'requested') {
        // 2. Check if user is an approver
        var approver = new GlideRecord('sysapproval_approver');
        approver.addQuery('sysapproval', current.sys_id);
        approver.addQuery('approver', gs.getUserID());
        approver.addQuery('state', 'requested');
        approver.query();

        if (approver.next()) {
          canApprove = true;
        }

        // 3. Or check if user has override role
        if (gs.hasRole('approval_admin')) {
          canApprove = true;
        }
      }

      canApprove;
    script: |
      // Approval logic
    comments: "Visible only when user can approve this request"

Phase 6: Client-Side vs Server-Side Scripts

Step 6.1: Client-Side Only (No Submit)

Action that only runs client-side:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Copy to Clipboard"
    table: incident
    action_name: copy_to_clipboard
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      // Pure client-side - no server round trip
      var number = g_form.getValue('number');
      var shortDesc = g_form.getValue('short_description');
      var text = number + ': ' + shortDesc;

      // Copy to clipboard (modern browsers)
      navigator.clipboard.writeText(text).then(function() {
        g_form.addInfoMessage('Copied to clipboard: ' + text);
      }).catch(function(err) {
        alert('Failed to copy: ' + err);
      });

      // Return false to prevent form submission
      return false;
    script: ""
    comments: "Copies incident number and description to clipboard"
Step 6.2: Server-Side Only

Action that only runs server-side:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Recalculate Priority"
    table: incident
    action_name: recalculate_priority
    order: 100
    active: true
    form_button: true
    client: false
    script: |
      // Server-side only
      var calculator = new PriorityCalculator();
      var newPriority = calculator.calculate(current.impact, current.urgency);

      current.priority = newPriority;
      current.work_notes = "Priority recalculated to " + newPriority;
      current.update();

      action.setRedirectURL(current);
    comments: "Recalculates priority based on impact and urgency matrix"
Step 6.3: Combined Client and Server

Client validates/confirms, then server executes:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Transfer to Team"
    table: incident
    action_name: transfer_to_team
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      // Client-side validation and data collection
      var group = g_form.getValue('assignment_group');
      if (!group) {
        g_form.showFieldMsg('assignment_group', 'Please select a group first', 'error');
        return false;
      }

      var reason = prompt('Enter transfer reason (required):');
      if (!reason || reason.trim() === '') {
        alert('Transfer reason is required');
        return false;
      }

      // Store reason in a hidden field or use sysparm
      g_form.setValue('work_notes', 'Transfer Reason: ' + reason);

      // Submit to server
      gsftSubmit(null, g_form.getFormElement(), 'transfer_to_team');
    script: |
      // Server-side execution
      var targetGroup = current.assignment_group.getDisplayValue();

      // Clear individual assignee
      current.assigned_to = '';

      // Add transfer audit trail
      current.work_notes = 'Transferred to ' + targetGroup + '\n' +
                           'Transferred by: ' + gs.getUserDisplayName();
      current.update();

      gs.addInfoMessage('Incident transferred to ' + targetGroup);
      action.setRedirectURL(current);
    comments: "Transfers incident with reason capture"

Phase 7: Using g_form in UI Actions

Step 7.1: Common g_form Methods

Reading Field Values:

// In onclick script
var value = g_form.getValue('field_name');
var display = g_form.getDisplayBox('field_name').value;
var reference = g_form.getReference('caller_id', function(ref) {
  // Async callback for reference fields
  alert('Caller name: ' + ref.name);
});

Setting Field Values:

// In onclick script
g_form.setValue('priority', '1');
g_form.setValue('work_notes', 'Updated via UI Action');
g_form.setDisplay('field_name', true);  // Show field
g_form.setMandatory('field_name', true);  // Make mandatory
g_form.setReadOnly('field_name', true);  // Make read-only

User Feedback:

// Messages
g_form.addInfoMessage('Operation completed successfully');
g_form.addWarningMessage('Please review the changes');
g_form.addErrorMessage('An error occurred');
g_form.showFieldMsg('field_name', 'Message text', 'info|warning|error');

// Clear messages
g_form.clearMessages();
g_form.hideFieldMsg('field_name');
Step 7.2: Complete Client Script Example
Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Validate and Submit"
    table: incident
    action_name: validate_submit
    order: 100
    active: true
    form_button: true
    client: true
    onclick: |
      function validateAndSubmit() {
        // Clear previous messages
        g_form.clearMessages();

        var valid = true;
        var errors = [];

        // Validate short description
        var shortDesc = g_form.getValue('short_description');
        if (!shortDesc || shortDesc.length < 10) {
          g_form.showFieldMsg('short_description', 'Must be at least 10 characters', 'error');
          valid = false;
          errors.push('Short description');
        }

        // Validate caller
        var caller = g_form.getValue('caller_id');
        if (!caller) {
          g_form.showFieldMsg('caller_id', 'Caller is required', 'error');
          valid = false;
          errors.push('Caller');
        }

        // Validate category
        var category = g_form.getValue('category');
        if (!category) {
          g_form.showFieldMsg('category', 'Category is required', 'error');
          valid = false;
          errors.push('Category');
        }

        // Check P1 requirements
        var priority = g_form.getValue('priority');
        if (priority == '1') {
          var businessImpact = g_form.getValue('business_impact');
          if (!businessImpact) {
            g_form.showFieldMsg('business_impact', 'Required for P1 incidents', 'error');
            valid = false;
            errors.push('Business Impact');
          }
        }

        if (!valid) {
          g_form.addErrorMessage('Please fix validation errors: ' + errors.join(', '));
          return false;
        }

        // Confirmation
        if (confirm('Submit this incident?')) {
          gsftSubmit(null, g_form.getFormElement(), 'validate_submit');
          return true;
        }

        return false;
      }

      validateAndSubmit();
    script: |
      current.update();
      gs.addInfoMessage('Incident saved successfully');
      action.setRedirectURL(current);
    comments: "Validates required fields before submission"

Phase 8: Redirecting After Action

Step 8.1: Redirect Methods

Redirect to Same Record:

// Server-side script
action.setRedirectURL(current);

Redirect to Different Record:

// Server-side script
var newRecord = new GlideRecord('incident');
newRecord.get('some_sys_id');
action.setRedirectURL(newRecord);

Redirect to List:

// Server-side script
action.setRedirectURL('incident_list.do?sysparm_query=active=true');

Redirect to Specific URL:

// Server-side script
action.setRedirectURL('/nav_to.do?uri=incident.do?sys_id=' + current.sys_id);

Prevent Redirect (Stay on Form):

// Server-side script
current.update();
action.setRedirectURL(current);  // Refresh current record

Redirect to Previous Page:

// Client-side onclick
// After server processing, redirect back
history.back();
return false;
Step 8.2: Common Redirect Patterns

Clone and Redirect to New:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ui_action
  data:
    name: "Clone Incident"
    table: incident
    action_name: clone_incident
    order: 100
    active: true
    form_button: true
    condition: "!current.isNewRecord()"
    client: false
    script: |
      // Clone the incident
      var clone = new GlideRecord('incident');
      clone.initialize();

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
37
Forks
13
Last commit
Jul 2026
Advanced
Catalog kind
skill
Gateway key
ui-actions
Source
github.com/happy-technologies-llc/happy-platform-skills