An application scope draws a boundary around one application’s tables, scripts and configuration. Inside the boundary, the app’s code can do what it likes with its own data. Crossing it, whether another app reading your tables or your app calling a script include in global, is governed by explicit settings. Interviewers ask about scopes to see whether you can design an app other teams can use without reaching into its internals, and whether you can debug the “Security restricted” messages that appear when a boundary blocks a call.
A scope is a namespace
Every scoped application has a namespace such as x_acme_fac (the x_, a company prefix, then an app identifier). Its artifacts carry the namespace: tables are named x_acme_fac_booking, script includes are called as x_acme_fac.RoomBookingApi, and roles, properties and events are prefixed too. Code in global is called with global., for example new global.SomeUtil().
That naming does two jobs. It prevents collisions (two vendors can both ship a Booking script include), and it lets the platform know, for every call, which application is asking and which application owns the target. Development follows the same boundary: you pick the application in the application picker, and changes are recorded against that app, which is why scoped apps move between instances as a unit through source control or the application repository rather than as loose global update sets.
Scoped code also runs against the scoped APIs. Some global conveniences aren’t available: gs.log() is replaced by gs.info(), gs.warn() and gs.error(), and GlideAjax.getXMLWait() isn’t offered at all. Business rules a scoped app creates on another scope’s tables are limited too: before rules there can only set field values, without scripts or aborting.
The target decides what’s reachable
The owning application controls how much of each artifact other scopes can touch:
- Script includes have an Accessible from setting: This application scope only or All application scopes. Internal helpers should be the former.
- Tables have application access settings: whether they’re accessible from all scopes, and if so, whether other scopes can read, create, update or delete records, whether they may add configuration (business rules, fields) to the table, and whether web services can reach it.
These settings are a ceiling. ServiceNow’s documentation notes that a privilege granted to another app can’t exceed what the table record itself allows: if the table doesn’t permit deletes from other scopes, no privilege record can grant one.
For finer control on the target side, Restricted Caller Access records let the target application allow or deny specific callers (a whole scope, or one script include, business rule or flow in it) to specific resources, with statuses requested, allowed, denied and invalidated (when the calling resource changes).
The caller’s runtime access tracking
On the calling side, each application has a runtime access tracking setting: None, Tracking or Enforcing. When it’s Tracking or Enforcing and the app’s code touches another application’s resource, the platform creates a cross-scope privilege record (sys_scope_privilege) naming the source scope, the target scope and name, the target type (table, script include or script object) and the operation (read, write, create, delete for tables; execute for scripts), with a status of requested, allowed or denied.
Tracking records what the app uses and lets it through, which is how developers discover the privileges their app needs by exercising it in a development instance. Enforcing only lets through calls with an allowed record. Once an app is published, only runtime requests that have a valid privilege record are allowed, so a code path nobody exercised during development fails in production.
Modelling a cross-scope call
This Node.js model (plain JavaScript reproducing the documented layers, not platform code) has an Onboarding app (x_acme_onb) using a Facilities app (x_acme_fac). Facilities exposes RoomBookingApi to all scopes, keeps BookingInternals private, and lets other scopes read, but not write, its booking table. One privilege record has already been approved, for the API.
// Cross-scope access, modelled in three layers:
// 1. the target's own setting (Accessible from, plus table operations),
// 2. runtime access tracking on the calling application,
// 3. cross-scope privilege records (source, target, operation, status).
const targets = {
'x_acme_fac.RoomBookingApi': { type: 'script include', accessibleFrom: 'all', ops: ['execute'] },
'x_acme_fac.BookingInternals': { type: 'script include', accessibleFrom: 'this', ops: ['execute'] },
'x_acme_fac_booking': { type: 'table', accessibleFrom: 'all', ops: ['read'] }, // Can read only
};
const privileges = [
{ source: 'x_acme_onb', target: 'x_acme_fac.RoomBookingApi', op: 'execute', status: 'allowed' },
];
function access(sourceScope, tracking, targetName, op) {
const t = targets[targetName];
if (t.accessibleFrom === 'this') return 'denied: accessible from this application scope only';
if (!t.ops.includes(op)) return `denied: the ${t.type} does not allow ${op} from other scopes`;
if (tracking === 'none') return 'allowed (no tracking)';
const p = privileges.find((x) => x.source === sourceScope && x.target === targetName && x.op === op);
if (p) return p.status === 'allowed' ? 'allowed by privilege record' : `denied: privilege is ${p.status}`;
privileges.push({ source: sourceScope, target: targetName, op, status: tracking === 'tracking' ? 'allowed' : 'requested' });
return tracking === 'tracking' ? 'allowed, privilege recorded as allowed' : 'denied, privilege recorded as requested';
}
const calls = [
['x_acme_fac.RoomBookingApi', 'execute'],
['x_acme_fac_booking', 'read'],
['x_acme_fac_booking', 'write'],
['x_acme_fac.BookingInternals', 'execute'],
];
for (const mode of ['tracking', 'enforcing']) {
privileges.splice(1); // reset to the one pre-approved record
console.log(`Onboarding app with runtime access tracking = ${mode}`);
for (const [target, op] of calls) console.log(` ${(op + ' ' + target).padEnd(37)} ${access('x_acme_onb', mode, target, op)}`);
}Onboarding app with runtime access tracking = tracking
execute x_acme_fac.RoomBookingApi allowed by privilege record
read x_acme_fac_booking allowed, privilege recorded as allowed
write x_acme_fac_booking denied: the table does not allow write from other scopes
execute x_acme_fac.BookingInternals denied: accessible from this application scope only
Onboarding app with runtime access tracking = enforcing
execute x_acme_fac.RoomBookingApi allowed by privilege record
read x_acme_fac_booking denied, privilege recorded as requested
write x_acme_fac_booking denied: the table does not allow write from other scopes
execute x_acme_fac.BookingInternals denied: accessible from this application scope onlyRead the two blocks side by side. The API call works in both modes because its privilege was approved. Reading the booking table works under Tracking, which quietly records an allowed privilege, but is blocked under Enforcing until an administrator approves the requested record; that’s the call that breaks after publishing if nobody exercised it beforehand. Writing the table and calling the internal helper fail in both modes, because the target never offered them. On a real instance, refusals show up in the system log as “Security restricted” entries naming the operation, the target and the calling scope, which is the first thing to search for when a cross-scope feature silently does nothing.
Expose an API, not your tables
Letting other apps read and write your tables directly couples them to your schema. Rename a field and three other teams’ flows break. The better design is a small, deliberately public script include that does validation and enforces your rules, while the tables stay readable at most:
// Facilities app (x_acme_fac). Script include, Accessible from: All application scopes
var RoomBookingApi = Class.create();
RoomBookingApi.prototype = {
initialize: function() {},
// Returns { ok: true, bookingId } or { ok: false, error }
book: function(roomId, startUtc, endUtc, forUserId) {
if (!/^[0-9a-f]{32}$/.test(roomId) || !/^[0-9a-f]{32}$/.test(forUserId)) {
return { ok: false, error: 'invalid id' };
}
var start = new GlideDateTime(startUtc), end = new GlideDateTime(endUtc);
if (!start.before(end)) return { ok: false, error: 'start must be before end' };
if (new BookingInternals().overlaps(roomId, start, end)) return { ok: false, error: 'room taken' };
var b = new GlideRecord('x_acme_fac_booking');
b.initialize();
b.setValue('room', roomId);
b.setValue('booked_for', forUserId);
b.setValue('start_time', start.getValue());
b.setValue('end_time', end.getValue());
return { ok: true, bookingId: b.insert() };
},
type: 'RoomBookingApi'
};The Onboarding app then calls it through the namespace:
// Onboarding app (x_acme_onb)
var result = new x_acme_fac.RoomBookingApi().book(roomId, startUtc, endUtc, newHire.getUniqueValue());
if (!result.ok) {
gs.warn('Desk booking failed for ' + newHire.getDisplayValue() + ': ' + result.error);
}Both snippets are illustrative and weren’t run on an instance. The shape is the point: the API validates its inputs, returns a predictable object instead of throwing, and uses BookingInternals (private to Facilities) for the logic other apps shouldn’t depend on. Facilities can now refactor its tables freely as long as book() keeps its contract. If the API ever has to change incompatibly, add bookV2() and retire the old method once callers have moved.
Habits that make scoped apps pleasant to live with
- Start in Tracking, finish in Enforcing. Exercise every code path in development so the privilege records exist, review them, and ship with Enforcing so surprises are blocked rather than silently allowed.
- Grant the minimum. If another app only needs to read, don’t tick create, update or delete on the table. Treat a privilege request from another team as a design conversation: would an API method be better?
- Keep global changes out. A scoped app that needs a dozen changes in global isn’t really scoped. Where global access is unavoidable, isolate it in one place and document why.
- Test as the other app. Cross-scope behaviour can only be verified by code running in the calling scope, so include those paths in automated tests.
Interview practice
A scoped Onboarding app worked in the development instance, but after it was installed in production, the step that books a desk for new hires does nothing, and there’s no error on the onboarding record. The Facilities app it calls is also scoped. Where do you look, and what’s the likely fix?
Answer
The pattern (works in development, silently fails after install) points to cross-scope access. I’d start with the system log in production around the time the step ran, searching for “Security restricted” entries from the x_acme_onb scope; they name the operation and target that were refused. Then I’d check the Onboarding app’s runtime access tracking setting and its cross-scope privilege records. In development the app probably ran under Tracking, which allows and records calls automatically; in production under Enforcing, a call with no allowed privilege record (for example, a read of a Facilities table that was only reached by a rarely run branch) is blocked, and the record sits in requested status.
The immediate fix is for an administrator to review and allow the specific privilege if it’s legitimate, or for the Facilities owners to check whether the table’s application access settings permit that operation at all. The better fix is in design: if Onboarding is reading or writing Facilities tables directly, replace that with a call to a public Facilities API, so there’s one privilege to grant and one contract to maintain. The silent failure is also a bug in itself: the onboarding step should check the API’s result and record an error on the onboarding task so someone sees it.
Related reading
- ServiceNow interview questions
- ServiceNow multiple-choice questions
- ServiceNow ACLs and Record Access Decisions, the user-level access layer that sits alongside scope rules
- ServiceNow Update Sets and Conflicts, for comparison with how global changes move
- Cross-scope privilege record (ServiceNow documentation)