Domain Separation for ServiceNow
Domain Separation enables multi-tenancy by partitioning data and processes between domains.
Domain Architecture TOP (Global) ├── Domain A (Customer 1) │ ├── Sub-domain A1 │ └── Sub-domain A2 └── Domain B (Customer 2) └── Sub-domain B1
Key Tables Table Purpose domain Domain definitions sys_user_has_domain User domain membership domain_path Domain hierarchy paths sys_db_object Table domain settings Domain Configuration (ES5) Create Domain // Create domain (ES5 ONLY!) var domain = new GlideRecord('domain'); domain.initialize();
domain.setValue('name', 'Acme Corp'); domain.setValue('description', 'Domain for Acme Corporation');
// Parent domain (empty for top-level) domain.setValue('parent', parentDomainSysId);
// Domain visibility domain.setValue('active', true);
domain.insert();
Domain-Aware Queries // Query respecting domain separation (ES5 ONLY!) function getDomainAwareRecords(tableName, query) { var gr = new GlideRecord(tableName);
// Domain separation is automatic when enabled
// Records are filtered to user's visible domains
if (query) {
gr.addEncodedQuery(query);
}
gr.query();
var records = [];
while (gr.next()) {
records.push({
sys_id: gr.getUniqueValue(),
sys_domain: gr.getValue('sys_domain'),
sys_domain_path: gr.getValue('sys_domain_path')
});
}
return records;
}
Cross-Domain Access // Access records across domains (requires elevated privileges) (ES5 ONLY!) function getCrossdomainRecords(tableName) { var gr = new GlideRecord(tableName);
// Disable domain separation for this query
gr.setQueryReferences(false);
// Query all domains
gr.queryNoDomain();
var records = [];
while (gr.next()) {
records.push({
sys_id: gr.getUniqueValue(),
domain: gr.sys_domain.getDisplayValue()
});
}
return records;
}
User Domain Membership (ES5) Assign User to Domain // Add user to domain (ES5 ONLY!) function addUserToDomain(userSysId, domainSysId, isPrimary) { // Check if already assigned var existing = new GlideRecord('sys_user_has_domain'); existing.addQuery('user', userSysId); existing.addQuery('domain', domainSysId); existing.query();
if (existing.next()) {
return existing.getUniqueValue();
}
// Create assignment
var assignment = new GlideRecord('sys_user_has_domain');
assignment.initialize();
assignment.setValue('user', userSysId);
assignment.setValue('domain', domainSysId);
assignment.setValue('primary', isPrimary);
return assignment.insert();
}
Get User's Domains // Get domains accessible to user (ES5 ONLY!) function getUserDomains(userSysId) { var domains = [];
var membership = new GlideRecord('sys_user_has_domain');
membership.addQuery('user', userSysId);
membership.query();
while (membership.next()) {
var domain = membership.domain.getRefRecord();
domains.push({
sys_id: domain.getUniqueValue(),
name: domain.getValue('name'),
is_primary: membership.getValue('primary') === 'true'
});
}
return domains;
}
Domain-Separated Tables (ES5) Configure Table for Domain Separation // Enable domain separation on table (ES5 ONLY!) // Note: This is typically done via UI, shown for reference
var tableConfig = new GlideRecord('sys_db_object'); if (tableConfig.get('name', 'u_custom_table')) { // Enable domain separation tableConfig.setValue('domain_separated', true);
// Domain separation type
// 'simple' = records belong to one domain
// 'containment' = records visible to parent domains
tableConfig.setValue('domain_id_type', 'simple');
tableConfig.update();
}
Create Record in Specific Domain // Create record in specific domain (ES5 ONLY!) function createInDomain(tableName, data, domainSysId) { var gr = new GlideRecord(tableName); gr.initialize();
// Set field values
for (var field in data) {
if (data.hasOwnProperty(field)) {
gr.setValue(field, data[field]);
}
}
// Set domain
gr.setValue('sys_domain', domainSysId);
return gr.insert();
}
Domain Picker (ES5) Get Available Domains for Picker // Get domains for domain picker widget (ES5 ONLY!) function getDomainsForPicker() { var domains = []; var userId = gs.getUserID();
// Get user's accessible domains
var membership = new GlideRecord('sys_user_has_domain');
membership.addQuery('user', userId);
membership.query();
while (membership.next()) {
var domain = membership.domain.getRefRecord();
if (domain.getValue('active') === 'true') {
domains.push({
sys_id: domain.getUniqueValue(),
name: domain.getValue('name'),
is_primary: membership.getValue('primary') === 'true',
is_current: domain.getUniqueValue() === gs.getSession().getCurrentDomainID()
});
}
}
// Sort: primary first, then alphabetically
domains.sort(function(a, b) {
if (a.is_primary && !b.is_primary) return -1;
if (!a.is_primary && b.is_primary) return 1;
return a.name.localeCompare(b.name);
});
return domains;
}
Switch Current Domain // Switch user's current domain (ES5 ONLY!) function switchDomain(domainSysId) { var session = gs.getSession();
// Verify user has access
var membership = new GlideRecord('sys_user_has_domain');
membership.addQuery('user', gs.getUserID());
membership.addQuery('domain', domainSysId);
membership.query();
if (!membership.next()) {
gs.addErrorMessage('You do not have access to this domain');
return false;
}
// Switch domain
session.setDomainID(domainSysId);
gs.addInfoMessage('Switched to domain: ' + membership.domain.getDisplayValue());
return true;
}
Domain Visibility Rules (ES5) Check Domain Visibility // Check if record is visible in current domain (ES5 ONLY!) function isRecordVisibleInDomain(tableName, recordSysId) { var gr = new GlideRecord(tableName); gr.addQuery('sys_id', recordSysId); gr.query();
// If record is found, it's visible in current domain context
return gr.hasNext();
}
Get Domain Path // Get full domain hierarchy path (ES5 ONLY!) function getDomainPath(domainSysId) { var path = [];
var domain = new GlideRecord('domain');
if (!domain.get(domainSysId)) {
return path;
}
// Build path from current to root
while (domain.isValidRecord()) {
path.unshift({
sys_id: domain.getUniqueValue(),
name: domain.getValue('name')
});
if (!domain.parent) break;
domain = domain.parent.getRefRecord();
}
return path;
}
MSP/Managed Services Patterns (ES5) Onboard New Tenant // Create new tenant domain with initial setup (ES5 ONLY!) function onboardTenant(tenantData) { // Create domain var domain = new GlideRecord('domain'); domain.initialize(); domain.setValue('name', tenantData.name); domain.setValue('parent', tenantData.parentDomain || ''); var domainSysId = domain.insert();
// Create tenant admin user
var adminUser = new GlideRecord('sys_user');
adminUser.initialize();
adminUser.setValue('user_name', tenantData.adminEmail);
adminUser.setValue('email', tenantData.adminEmail);
adminUser.setValue('first_name', tenantData.adminFirstName);
adminUser.setValue('last_name', tenantData.adminLastName);
var adminSysId = adminUser.insert();
// Assign user to domain
addUserToDomain(adminSysId, domainSysId, true);
// Assign tenant admin role
var role = new GlideRecord('sys_user_has_role');
role.initialize();
role.setValue('user', adminSysId);
role.setValue('role', getTenantAdminRoleSysId());
role.insert();
return {
domain_sys_id: domainSysId,
admin_sys_id: adminSysId
};
}
MCP Tool Integration Available Tools Tool Purpose snow_query_table Query domain-aware data snow_execute_script_with_output Test domain scripts snow_find_artifact Find domain configurations Example Workflow // 1. Query domains await snow_query_table({ table: 'domain', query: 'active=true', fields: 'name,parent,sys_id' });
// 2. Get user domain memberships await snow_query_table({ table: 'sys_user_has_domain', query: 'user=user_sys_id', fields: 'domain,primary' });
// 3. Check domain-separated tables await snow_query_table({ table: 'sys_db_object', query: 'domain_separated=true', fields: 'name,label,domain_id_type' });
Best Practices Plan Hierarchy - Design domain structure before implementation Minimal Domains - Only create necessary separation User Access - Assign minimum required domains Testing - Test with domain picker Global Data - Keep shared data in TOP domain Performance - Domain queries add overhead Documentation - Document domain purposes ES5 Only - No modern JavaScript syntax