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 through matching table-level and field-level rules for the requested operation. Inside a single ACL, the role, condition, and script requirements must all pass for that ACL to grant access.
The practical rule is this: a user must pass the required table ACL and, for field access, the required field ACL. When multiple ACLs match at the same specificity, a passing Allow If ACL can grant access, while Deny Unless ACLs are evaluated first and can block access.
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 a simple reference table you own. No roles, no condition, no script. Use this pattern for low-risk custom lookup data, not for sensitive platform tables.
Table: x_acme_location
Operation: read
Roles: (empty)
Condition: (empty)
Script: (empty)
// Result: any authenticated user can read Location lookup 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.getValue('subject_person') == userId) { answer = true; return; }
var subject = new GlideRecord('sys_user');
if (subject.get(current.getValue('subject_person'))) {
answer = (subject.getValue('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: x_acme_employee.salary
Operation: read
Roles: hr_admin
Condition: (empty)
Script: (empty)
// Anyone without hr_admin cannot read this field.The table.* ACL covers fields not explicitly defined. A named field ACL is more specific, but the user still needs the required table-level access for the operation.
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.getValue('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: Sensitive State Guard
Allow write for normal records but block edits once a record reaches a locked state. Use this for data-dependent write controls where the rule belongs with the record, not with a button or list action.
Table: x_acme_financial_record
Operation: write
Roles: finance_user
Condition: (empty)
Script:
(function() {
// Only finance admins can edit locked records.
if (current.getValue('state') == 'locked' && !gs.hasRole('finance_admin')) {
answer = false;
gs.addErrorMessage('Locked financial records 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.