What is an ACL and why it matters
An ACL (Access Control List) is a rule that determines whether a user can perform a specific operation on a specific object — a table, a record, a field, or a UI element. Every time a user views a list, opens a record, edits a field, or clicks a UI action button, ServiceNow evaluates applicable ACLs to determine whether the operation is allowed.
The default stance is deny. If no ACL matches a user's access attempt, access is denied. ACLs grant access — they do not restrict from a permissive baseline. This secure-by-default model means that custom tables and fields you create are protected until you explicitly grant access.
ACL types
ACLs can be applied at five different levels:
- Table-level ACLs — control access to an entire table for all records. The most common type. Example: who can read the incident table at all.
- Record-level ACLs — control access to specific records based on the record's data. Uses script conditions. Example: users can only see incidents assigned to their group.
- Field-level ACLs — control access to a specific field on a table. Example: only HR managers can read or write the salary field on sys_user.
- UI Action ACLs — control visibility of buttons, context menu items, and form actions. Example: only change managers can see the "Approve Change" button.
- Business Rule ACLs — rarely used directly; controls whether a Business Rule can be seen in the development interface.
ACL operations
Each ACL is associated with one of these operations:
- read — can the user view this record or field?
- write — can the user modify this field?
- create — can the user insert new records on this table?
- delete — can the user delete records from this table?
- execute — used for UI Actions — can the user see and click this button?
A user needs separate ACL permissions for each operation. Having read access to a table does not automatically grant write access.
How ACL evaluation works — the evaluation sequence
Understanding evaluation order explains why some ACLs seem to be ignored and why unexpected access issues occur.
- ServiceNow identifies all ACLs that could match the object being accessed (e.g., all read ACLs on the incident table or the incident.state field)
- ACLs are sorted by specificity — more specific ACLs evaluate first: field-level before table-level, specific role before wildcard role
- Each matching ACL is evaluated — Role AND Condition AND Script must all pass
- The first ACL that explicitly grants access allows the operation
- If no ACL grants access, access is denied
Specificity rules:
incident.state(field ACL) evaluates beforeincident.*(all fields ACL)incident(table ACL) evaluates beforetask(parent table ACL)- A user needs to pass both the table read ACL and the field read ACL to see a field's value
The three conditions — all must pass
Each ACL has three independent conditions. All three must evaluate to true for the ACL to grant access. If any one fails, the ACL denies access regardless of the others.
1. Roles
The user must have at least one of the roles listed on the ACL. If no roles are specified, any authenticated user passes the role check. If roles are specified, the user must have at least one of them — it is an OR condition, not AND.
// ACL roles: itil, admin
// User passes if they have itil OR admin (not required to have both)
2. Condition
A filter condition evaluated against the record. Works like an encoded query condition — if the record does not match the condition, the ACL does not apply. Leave empty for "always apply."
// ACL condition on incident table:
// State = New OR State = In Progress
// This ACL only applies to open incidents — not resolved or closed ones
3. Script
A server-side script that returns true to grant access or false to deny. Use for dynamic access control that cannot be expressed as a role check or filter condition alone.
// ACL Script field — must evaluate answer to true or false
// Example: Allow write access only to assigned agent or admin
// Method 1 — set answer variable
answer = gs.hasRole('itil_admin') || (current.assigned_to == gs.getUserID());
// Method 2 — use a function that returns true/false
(function() {
// Allow if user is the assigned agent
if (current.assigned_to == gs.getUserID()) return true;
// Allow if user has the admin role
if (gs.hasRole('admin')) return true;
// Allow if user is in the assignment group
var gr = new GlideRecord('sys_user_grmember');
gr.addQuery('user', gs.getUserID());
gr.addQuery('group', current.assignment_group);
gr.query();
return gr.next();
})()
Field-level ACLs
Field-level ACLs are one of the most commonly missed security requirements. Securing a table with read ACLs does not protect individual fields on that table. A user with table read access can see all fields unless specific field-level ACLs restrict them.
// Table: sys_user (Users)
// Table-level read ACL: authenticated users can read
// But without field-level ACLs, everyone can see salary, SSN, personal data
// Field-level ACL examples needed:
// sys_user.u_salary — read: hr_manager role only
// sys_user.u_ssn — read: admin role only
// sys_user.u_medical_info — read: hr_medical role only
Always audit sensitive fields when setting up table ACLs. Ask: if a user can read this record, are there fields they should not see? Those fields need their own field-level ACLs.
Script ACL patterns
Record ownership — user can only access their own records
// ACL on: incident, operation: read
// Allow if user is the caller, assigned agent, or has itil role
answer = gs.hasRole('itil')
|| current.caller_id == gs.getUserID()
|| current.assigned_to == gs.getUserID();
Group-based access — user in the assignment group
// Allow if user is in the assignment group of the record
answer = gs.hasRole('admin') || isInGroup(current.assignment_group.toString());
function isInGroup(groupId) {
if (!groupId) return false;
var gr = new GlideRecord('sys_user_grmember');
gr.addEncodedQuery('user=' + gs.getUserID() + '^group=' + groupId);
gr.query();
return gr.next();
}
Time-based access — access only during business hours
// Allow admin operations only during business hours
var hour = new GlideDateTime().getLocalTime().getByFormat('HH');
var hourInt = parseInt(hour);
answer = gs.hasRole('admin') && hourInt >= 8 && hourInt < 18;
Performance in ACL scripts — critical warning
ACL scripts execute on every single record access. If an ACL script contains a GlideRecord query, that query runs for every record in every list view, every API response, and every form load that involves records from that table.
On an incident list with 1,000 results, a GlideRecord query inside the incident ACL script runs 1,000 times. This is a significant performance problem.
// BAD — GlideRecord query inside ACL script runs on every single record
answer = gs.hasRole('itil');
var gr = new GlideRecord('u_access_config'); // This runs per-record
gr.addQuery('user', gs.getUserID());
gr.query();
if (gr.next()) answer = true;
// BETTER — use gs.hasRole() and current field comparisons only
// These are evaluated without additional database queries
answer = gs.hasRole('itil') || current.caller_id == gs.getUserID();
// If you must query, cache the result in a session variable
var cacheKey = 'acl_cache_' + gs.getUserID();
var cached = gs.getSessionClientData(cacheKey);
if (cached !== null) {
answer = (cached == 'true');
} else {
var hasAccess = checkAccessFromDB(gs.getUserID());
gs.putSessionClientData(cacheKey, hasAccess.toString());
answer = hasAccess;
}
Debugging ACL issues
The Security Debug plugin
The fastest way to diagnose ACL issues:
- Navigate to System Diagnostics > Security Policy Check (or search "security" in navigator)
- Enter the sys_id of the record the user cannot access
- Enter the user's sys_id (impersonate them or use their sys_id)
- Select the operation (read, write, create, delete)
- Click Evaluate
The output shows every ACL that was evaluated, in what order, and which condition failed — the role check, the condition, or the script. This tells you exactly why access was denied in under a minute.
URL debug parameter
Add &sysparm_security_debug=true to any URL to see ACL debug output directly in the page. This shows which ACLs matched for the records on that page, inline in the response.
Testing as the actual user
Testing ACLs as admin with impersonation is unreliable because admin bypasses many ACL checks — including ACLs that do not list admin in their roles (admin has implicit access to most things). Always test in a separate browser session logged in as the actual user, or as a test account with exactly the same roles as the user experiencing the issue.
// This is the most common ACL testing mistake:
// Developer impersonates user A, tests as admin → "I can access it, so it works"
// User A logs in → "I cannot access it"
// Admin implicit permissions bypass ACLs that user A would fail
// Correct testing:
// Log in directly as user A (or a test account with identical roles)
// Test the exact operation that fails
Common security mistakes
Mistake 1 — Using admin as ACL bypass
// Dangerous pattern
answer = gs.hasRole('admin') || someCondition;
// If admin access scope expands, this ACL exposes data unintentionally
// Better — be explicit about which roles should have access
answer = gs.hasRole('itil_admin') || someCondition;
Mistake 2 — Missing field-level ACLs on sensitive data
Always add field-level ACLs for: salary, compensation, SSN, medical data, personal identification, passwords/credentials, private notes.
Mistake 3 — ACL conditions too broad
An ACL with no condition applies to every record on the table — including records from other companies in multi-tenant deployments, historical records, and records in states the ACL was not intended to cover. Always add conditions that scope the ACL to exactly the records it should apply to.
Mistake 4 — Not reviewing ACLs during upgrades
ServiceNow upgrades can add new fields to existing tables, modify role definitions, or change how built-in ACLs evaluate. Review security-critical ACLs after every upgrade to ensure they still behave as expected.
Mistake 5 — Over-relying on UI visibility for security
Hiding a field in a form view or making it read-only with a UI Policy does not prevent access via the REST API or direct table access. Security must be enforced at the ACL layer — UI restrictions are for usability, not security.
ACL best practices
- Create custom roles for your applications — do not re-use built-in roles for custom access control. Create a role like
x_myapp_userand grant it exactly the access needed. - Document what each custom role grants — add a description to every custom role record: "Grants read access to customer service tables and write access to case records assigned to the user's group."
- Keep ACL scripts lightweight — gs.hasRole() and current field comparisons only. No GlideRecord queries unless cached.
- Test as the actual user — not as admin with impersonation.
- Review ACLs in every release — they accumulate technical debt. Old ACLs that no longer apply should be cleaned up.
- Never leave admin-created tables without ACLs — any table created in the global scope without explicit ACLs is accessible to any user with the admin role.
Related guides:
- Roles vs Groups in ServiceNow — how roles and groups work together
- ACL Security Guide — deeper patterns and advanced scenarios
- Debugging scripts — debugging ACL script logic
- Scoped applications — ACL scoping in custom applications