This skill should be used when the user asks to "domain separation", "multi-tenant", "domain path", "domain visibility", "domain picker", "MSP", "managed services", or any ServiceNow Domain Separation development.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: domain-separation description: This skill should be used when the user asks to "domain separation", "multi-tenant", "domain path", "domain visibility", "domain picker", "MSP", "managed services", or any ServiceNow Domain Separation development. license: Apache-2.0 compatibility: Designed for Snow-Code and ServiceNow development metadata: author: groeimetai version: "1.0.0" category: servicenow tools:
- snow_query_table
- snow_execute_script_with_output
- snow_find_artifact
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
More by groeimetai
View allThis skill should be used when the user asks to "create flow", "Flow Designer", "workflow automation", "subflow", "action", "flow trigger", "scheduled flow", or any ServiceNow Flow Designer development.
This skill should be used when the user asks to "create knowledge article", "KB article", "knowledge base", "knowledge workflow", "article template", "publish article", or any ServiceNow Knowledge Management development.
This skill should be used when the user asks to "create script include", "utility class", "reusable code", "server-side library", "AbstractAjaxProcessor", "GlideAjax", "client callable", or any ServiceNow Script Include development.
This skill should be used when the user asks to "HR case", "employee center", "onboarding", "offboarding", "HR service", "lifecycle event", "HR catalog", or any ServiceNow HR Service Delivery development.
