← Home
🔥0 DAY
0 XP
Scripting Recipe · ACLs

ACL SCRIPT EXAMPLES
BY TABLE & OPERATION.

Every ServiceNow ACL script does one job: assign answer. What changes is the table, the operation, and what is safe to read from current at that moment. Eight copy-ready examples below, each with the reason it is written that way and the mistake interviewers listen for.

Answer summary

  • ACL scripts set answer; a return value is ignored.
  • Order of evaluation is most specific first: table.field table.* table. All matching levels must pass.
  • Roles, conditions and script all have to pass — they are ANDed, not ORed.
  • On create, current is empty: decide from the user. On read/write/delete, decide from current without extra queries.
  • Hierarchy tables (cmdb_ci, task) inherit ACLs down to every extended class — write the rule at the base.
  1. incidentread

    Only the assignee, the caller, or an admin may read an incident.

    answer = false;
    var me = gs.getUserID();
    if (gs.hasRole('admin')
        || current.assigned_to == me
        || current.caller_id == me) {
      answer = true;
    }

    WhyRow-level read ACLs run per record, so keep them cheap — compare sys_ids already on `current`, never run a GlideRecord query here.

    PitfallA read ACL that queries another table multiplies by list size and will time out on a 10k-row list view.

  2. incident.statewrite (field level)

    Only itil users can move an incident out of New once it is assigned.

    answer = gs.hasRole('admin');
    if (!answer && gs.hasRole('itil')) {
      answer = !current.assignment_group.nil();
    }

    WhyField ACLs (table.field) evaluate after the table-level ACL passes. Both must return true, so field ACLs only ever narrow access.

    PitfallDenying at the field level does not deny the row. If the user must not see the record at all, write the rule on `incident` itself.

  3. sc_requestcreate

    Any authenticated, active user may raise a request; guests may not.

    answer = false;
    if (gs.isLoggedIn() && !gs.getUser().isMemberOf('Blocked Requesters')) {
      answer = true;
    }

    WhyOn create, `current` is an empty record — field values are not reliable yet. Base the decision on the user, not on record data.

    PitfallReading `current.requested_for` in a create ACL returns an empty string, so conditions on it silently grant or deny for everyone.

  4. cmdb_cidelete

    CIs must never be deleted by ITIL users — only by CMDB admins, and only when retired.

    answer = false;
    if (gs.hasRole('cmdb_admin') && current.install_status == '7') {
      answer = true;
    }

    WhyDelete ACLs on `cmdb_ci` inherit down every extended class, so one rule covers cmdb_ci_computer, cmdb_ci_server, and the rest.

    PitfallWriting the ACL on the child class only (cmdb_ci_server) leaves every sibling class unprotected.

  5. sys_user.emailwrite (field level)

    Users may edit their own email; only user_admin may edit anyone else's.

    answer = gs.hasRole('user_admin')
           || current.sys_id == gs.getUserID();

    WhySelf-service ACLs compare the record's sys_id with the session user — the cheapest possible check and no query.

    PitfallComparing `current.user_name == gs.getUserName()` breaks when the user name changes; always compare sys_ids.

  6. sys_ui_action (execute)execute

    Only change managers may run the 'Force Close' UI action.

    answer = gs.hasRole('change_manager')
           && current.state != '3';

    WhyExecute-type ACLs guard UI actions, processors, and client-callable Script Includes. They gate the operation, not a row.

    PitfallHiding the button with a UI action condition is not security — without an execute ACL the endpoint is still callable.

  7. u_custom_table.*read / write

    A custom table needs one rule covering every field except a sensitive one.

    // u_custom_table.* (wildcard)
    answer = gs.hasRole('u_custom_reader');
    
    // u_custom_table.u_ssn (specific, overrides the wildcard)
    answer = gs.hasRole('u_custom_pii');

    WhyEvaluation order is most-specific first: table.field beats table.*, which beats table. The first matching ACL for that specificity level decides.

    PitfallAssuming ACLs OR together across specificity levels. They do not — a specific field ACL that denies wins over a permissive wildcard.

  8. any tableany

    When should the logic live in the Condition builder instead of the script?

    // Prefer this (condition field): assignment_group.manager = javascript:gs.getUserID()
    // Script only for what conditions cannot express:
    answer = new global.AclHelper().canEdit(current, gs.getUserID());

    WhyConditions are indexed and evaluated in the query layer; scripts run per row in Rhino. Push filtering into the condition and keep the script for genuine logic.

    PitfallA Script Include called from an ACL must be client-callable = false and should cache per-request, or you re-run the same query for every row.

Keep going

ACL scripts lean on the same GlideRecord fundamentals — reference fields, dot-walking, and query cost. These two guides pair directly with this one.