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