ServiceNow ACL Examples: 12 Production-Ready Patterns
Twelve battle-tested ACL patterns for the situations you actually hit in production: role gates, scripted checks, field redaction, scoped app boundaries, and more.
How ACL Evaluation Works
ServiceNow ACLs are evaluated in a specific order: table-level ACLs run first, then field-level ACLs on the same operation. Within a level, all matching ACLs must allow the operation (Roles, Condition, Script) for the user to be granted access. A single failing component denies the operation.
The result is conjunction at the ACL level, disjunction across multiple ACLs at the same scope - in plain English: any matching ACL that passes all three checks (roles, condition, script) is enough, but inside a single ACL all three must pass.
Every pattern below shows the table, the operation, and the three components. Copy them, adjust the role names, and ship.
Pattern 1: Public Read on a Reference Table
Any authenticated user can read sys_user. No roles, no condition, no script. The simplest possible ACL.
Table: sys_user
Operation: read
Roles: (empty)
Condition: (empty)
Script: (empty)
// Result: any authenticated user can read User records.Pattern 2: Role-Gated Write
Only users with itil can update Incident. The most common ACL pattern across ServiceNow.
Table: incident
Operation: write
Roles: itil
Condition: (empty)
Script: (empty)Pattern 3: Multi-Role OR Gate
Roles entered together are OR'd. Any one of them is enough. Use this for the "ITIL agents or service desk leads" case.
Table: incident
Operation: write
Roles: itil, sn_incident_write
Condition: (empty)
Script: (empty)Pattern 4: Conditional ACL (Field-Driven)
Allow read only when the user is the caller, or the record is not marked confidential. The condition runs against current.
Table: incident
Operation: read
Roles: itil
Condition: caller_id=javascript:gs.getUserID()^ORconfidential=false
Script: (empty)A common audit finding is using a Condition that compares strings without javascript: prefix, producing literal-text comparisons that never match. Always use javascript: for dynamic comparisons.
Pattern 5: Scripted ACL with Helper Lookup
Sometimes the eligibility rule depends on data not present on current. A script ACL handles the lookup. Always assign answer; do not just return.
Table: hr_case
Operation: read
Roles: hr_basic
Condition: (empty)
Script:
(function() {
// Only allow read if the user is in the same department.
var u = new GlideRecord('sys_user');
if (!u.get(gs.getUserID())) {
answer = false;
return;
}
answer = (u.getValue('department') == current.getValue('department'));
})();Pattern 6: Owner-Or-Manager Read
Users can read their own records. Their managers can read their team's records. Anyone with hr_admin can read everything.
Table: hr_case
Operation: read
Roles: (empty)
Condition: (empty)
Script:
(function() {
if (gs.hasRole('hr_admin')) { answer = true; return; }
var userId = gs.getUserID();
if (current.subject_person == userId) { answer = true; return; }
var subject = new GlideRecord('sys_user');
if (subject.get(current.subject_person.toString())) {
answer = (subject.manager == userId);
return;
}
answer = false;
})();Pattern 7: Field-Level ACL (Hide Salary)
Hide a sensitive field from everyone except a specific role. Field-level ACLs are written with the field name in the Name column.
Table: sys_user.salary
Operation: read
Roles: hr_admin
Condition: (empty)
Script: (empty)
// Anyone without hr_admin sees the field as ********The table.* ACL covers fields not explicitly defined. Field-level ACLs override the wildcard for the named field only.
Pattern 8: table.None for Operation Gate
The table.None ACL gates the entire operation against the table regardless of record. Use it to disable an operation outright for non-privileged users.
Table: cmdb_ci.None
Operation: delete
Roles: cmdb_admin
Condition: (empty)
Script: (empty)
// Only cmdb_admin can delete any CIPattern 9: Read-After-Approval
Common in HR and procurement: a record is only visible to general users after an approval is granted. The Condition uses a state field.
Table: hr_offer
Operation: read
Roles: (empty)
Condition: state=approved^ORassigned_to=javascript:gs.getUserID()
Script: (empty)Pattern 10: Scoped App Boundary
Restrict access to a custom table to roles defined within the scoped application. Use the scoped role plus a delegated admin check.
Table: x_acme_pmo_project
Operation: write
Roles: x_acme_pmo.project_manager
Condition: (empty)
Script:
(function() {
if (gs.hasRole('x_acme_pmo.delegated_admin')) {
answer = true;
return;
}
// Project managers can only edit their own projects.
answer = (current.project_manager == gs.getUserID());
})();Pattern 11: Cross-Table Reference Check
Allow read on a CMDB CI only if the user is a member of the support group on the related Service Offering. Cross-table references are where scripted ACLs earn their cost.
Table: cmdb_ci
Operation: read
Roles: itil
Condition: (empty)
Script:
(function() {
if (gs.hasRole('cmdb_admin')) { answer = true; return; }
var offering = new GlideRecord('service_offering');
offering.addQuery('cmdb_ci', current.sys_id);
offering.query();
while (offering.next()) {
var supportGroup = offering.getValue('support_group');
if (supportGroup && gs.getUser().isMemberOf(supportGroup)) {
answer = true;
return;
}
}
answer = false;
})();Pattern 12: Mass Update Guard
Allow write but block mass updates from the list view. Useful on tables where individual record changes are fine but bulk changes carry risk. ServiceNow surfaces the bulk context as gs.action.
Table: financial_record
Operation: write
Roles: finance_user
Condition: (empty)
Script:
(function() {
// Block mass updates from the list view for non-admins.
var action = gs.action ? gs.action.getGlideURI() + '' : '';
if (action.indexOf('sys_target=true') !== -1 && !gs.hasRole('finance_admin')) {
answer = false;
gs.addErrorMessage('Mass updates require finance_admin.');
return;
}
answer = true;
})();Five ACL Anti-Patterns
- Return without setting answer. Scripted ACLs only honor the answer variable.
return truedoes nothing. - Empty table-level + restrictive field-level. If the table.None denies, no field ACL can grant. Set the broad case first.
- Conditions without javascript:. A literal string comparison never matches a dynamic user ID.
- Scripted ACLs hitting GlideRecord for every row. A list view of 50 records means 50 ACL evaluations. Cache anything you can.
- Granting maint or admin to work around an ACL. The 500+ checkpoint Instance Audit flags this immediately.
For a deeper security read, see the broader Instance Auditing overview.
AI-Assisted ACL Generation
Writing ACLs by hand is fine for the simple cases. For scripted ACLs with cross-table references and role hierarchies, an AI assistant grounded in ServiceNow specifics saves real time. SnowCoder is 60% more accurate than generic LLMs across 120+ ServiceNow benchmarks, because the 100,000+ vector knowledge base includes the platform's actual security guidance, not just public web text.
The Security hat in Yeti AI Chat is purpose-built for ACL review. Ask it to audit an existing ACL set against the 500+ Instance Audit checkpoints, or generate a new one against your scope. See the Yeti Chat overview for the four hats and four modes.
Related Reading
Generate audited ACLs in seconds
Describe the access rule. SnowCoder generates the ACL, checks it against the 500+ Instance Audit checkpoints, and flags risks before you commit.