| name | domain-separation |
| description | Configure ServiceNow multi-tenant domain separation — domain hierarchy, sys_user_has_domain membership, domain-aware vs cross-domain queries, MSP tenant onboarding, and domain picker logic. |
| license | Apache-2.0 |
| compatibility | Designed for Snow-Code and ServiceNow development |
| metadata | {"author":"serac","version":"1.0.0","category":"servicenow"} |
| tools | ["snow_query_table","snow_execute_script","snow_artifact_manage"] |
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
var domain = new GlideRecord("domain")
domain.initialize()
domain.setValue("name", "Acme Corp")
domain.setValue("description", "Domain for Acme Corporation")
domain.setValue("parent", parentDomainSysId)
domain.setValue("active", true)
domain.insert()
Domain-Aware Queries
function getDomainAwareRecords(tableName, query) {
var gr = new GlideRecord(tableName)
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
function getCrossdomainRecords(tableName) {
var gr = new GlideRecord(tableName)
gr.setQueryReferences(false)
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
function addUserToDomain(userSysId, domainSysId, isPrimary) {
var existing = new GlideRecord("sys_user_has_domain")
existing.addQuery("user", userSysId)
existing.addQuery("domain", domainSysId)
existing.query()
if (existing.next()) {
return existing.getUniqueValue()
}
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
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
var tableConfig = new GlideRecord("sys_db_object")
if (tableConfig.get("name", "u_custom_table")) {
tableConfig.setValue("domain_separated", true)
tableConfig.setValue("domain_id_type", "simple")
tableConfig.update()
}
Create Record in Specific Domain
function createInDomain(tableName, data, domainSysId) {
var gr = new GlideRecord(tableName)
gr.initialize()
for (var field in data) {
if (data.hasOwnProperty(field)) {
gr.setValue(field, data[field])
}
}
gr.setValue("sys_domain", domainSysId)
return gr.insert()
}
Domain Picker (ES5)
Get Available Domains for Picker
function getDomainsForPicker() {
var domains = []
var userId = gs.getUserID()
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(),
})
}
}
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
function switchDomain(domainSysId) {
var session = gs.getSession()
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
}
session.setDomainID(domainSysId)
gs.addInfoMessage("Switched to domain: " + membership.domain.getDisplayValue())
return true
}
Domain Visibility Rules (ES5)
Check Domain Visibility
function isRecordVisibleInDomain(tableName, recordSysId) {
var gr = new GlideRecord(tableName)
gr.addQuery("sys_id", recordSysId)
gr.query()
return gr.hasNext()
}
Get Domain Path
function getDomainPath(domainSysId) {
var path = []
var domain = new GlideRecord("domain")
if (!domain.get(domainSysId)) {
return path
}
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
function onboardTenant(tenantData) {
var domain = new GlideRecord("domain")
domain.initialize()
domain.setValue("name", tenantData.name)
domain.setValue("parent", tenantData.parentDomain || "")
var domainSysId = domain.insert()
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()
addUserToDomain(adminSysId, domainSysId, true)
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 | Test domain scripts |
snow_artifact_manage | Find domain configurations (action='find') |
Example Workflow
await snow_query_table({
table: "domain",
query: "active=true",
fields: "name,parent,sys_id",
})
await snow_query_table({
table: "sys_user_has_domain",
query: "user=user_sys_id",
fields: "domain,primary",
})
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