While working on a ServiceNow Orchestration flow for cross-domain Active Directory user management, we needed to validate whether a user from one domain (say, domain1) could be added to a group in another domain (domain2) — including both local and universal group scopes — with proper error handling covering the various ways this can fail.
The short answer: yes, this is feasible via PowerShell, provided the necessary trust relationships and permissions are in place between the domains. Here's the approach, the group-scope rules that actually govern what's possible, and a working script with error handling built in.
Prerequisites
- Active Directory Module for PowerShell — included with Remote Server Administration Tools (RSAT) on Windows Server, or installable separately on Windows 10/11.
- Trust relationship between the two domains (external, forest, or another trust type, depending on your topology).
- Administrative permissions sufficient to perform the operation on both the source and target domains.
Understanding Group Scope — This Determines What's Actually Possible
Before troubleshooting a script, it's worth understanding that some cross-domain scenarios aren't a scripting problem at all — they're a fundamental Active Directory limitation:
- Domain Local groups can contain members from any domain in the forest (or trusted external domains), but the group itself can only be used to grant permissions within the domain where it's defined.
- Global groups can only contain members from the same domain as the group — they cannot have cross-domain members at all.
- Universal groups can contain members from any domain in the forest, and can be granted permissions anywhere in the forest.
The practical takeaway: if you need true cross-domain group membership, it has to be a Universal group. If your script is failing to add a cross-domain member to a Domain Local or Global group specifically, that's very likely AD enforcing scope rules correctly — not a bug to fix in the script.
# Universal group: can contain members from any domain in the forest
New-ADGroup -Name "UniversalGroup" -GroupScope Universal -GroupCategory Security -Path "CN=Users,DC=domain2,DC=corp,DC=local"
Add-ADGroupMember -Identity "UniversalGroup" -Members "user1@domain1.corp.local", "user2@domain2.corp.local"
# Domain Local group: members can be cross-domain, but the group only works within its own domain
New-ADGroup -Name "DomainLocalGroup" -GroupScope DomainLocal -GroupCategory Security -Path "CN=Users,DC=domain1,DC=corp,DC=local"
Add-ADGroupMember -Identity "DomainLocalGroup" -Members "user1", "user2"
Setting Up the MID Server for PowerShell Execution
The MID Server is what lets ServiceNow execute commands and gather data in your local environment — it's the bridge between the instance and your on-premises Active Directory.
- Install and configure the MID Server per ServiceNow's documentation, ensuring it has network access to both the ServiceNow instance and the domain controllers it needs to reach.
- Write your PowerShell script for the AD operation you need.
- Upload the script: Navigate to MID Server Script Files and create a new record with your script attached.
- Create a Probe: Navigate to MID Server > Probes, create a new probe of type PowerShell, and reference your uploaded script.
- Create a Command: Navigate to MID Server > Commands, create a command that references your probe, and define the parameters your script expects (username, sourceDomain, targetDomain, etc.).
- Trigger it from a business rule, script, or UI action.
Triggering via ecc_queue (Classic Orchestration)
var gr = new GlideRecord('ecc_queue');
gr.initialize();
gr.agent = 'MIDServerName'; // Replace with your MID Server name
gr.topic = 'AddUserToGroupProbe'; // Name of your probe
gr.name = 'AddUserToGroupCommand'; // Name of your command
gr.source = 'script';
gr.payload = JSON.stringify({
username: 'user123',
sourceDomain: 'domain1.com',
targetDomain: 'domain2.com',
targetGroupName: 'GroupName',
sourceCred: 'sourceCredential',
targetCred: 'targetCredential'
});
gr.insert();
Worth knowing: this direct ecc_queue insert pattern is a classic Orchestration technique, tied to the legacy Workflow engine. It still works and is documented, but if you're building new automation today, the more current path is Flow Designer — either a custom action calling the PowerShellProbe/IPaaSActionProbe API, or the Microsoft AD spoke, which wraps this same MID Server mechanism in a Flow Designer-native action. And if your organization has migrated from on-premises AD to Entra ID, note that the Entra ID spoke doesn't require a MID Server at all — it talks to Microsoft Graph directly, since there's no on-prem domain controller to reach. Worth confirming which directory service you're actually integrating with before assuming you need this MID Server setup at all.
A Complete PowerShell Script With Error Handling
This script validates inputs, checks connectivity to both domains, retrieves the user, attempts the group add, and logs every step for troubleshooting.
param (
[string]$username,
[string]$sourceDomain,
[string]$targetDomain,
[string]$groupName,
[pscredential]$sourceCred,
[pscredential]$targetCred,
[string]$logFile = "C:\Scripts\ADUserAdd.log"
)
function Write-Log {
param ([string]$message, [string]$logFile)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "$timestamp - $message"
Add-Content -Path $logFile -Value $logMessage
}
function Validate-Parameters {
param ([string]$username, [string]$sourceDomain, [string]$targetDomain, [string]$groupName, [string]$logFile)
Write-Log -message "Validating parameters." -logFile $logFile
if (-not $username) { $msg = "Username is required."; Write-Log -message $msg -logFile $logFile; throw $msg }
if (-not $sourceDomain) { $msg = "Source domain is required."; Write-Log -message $msg -logFile $logFile; throw $msg }
if (-not $targetDomain) { $msg = "Target domain is required."; Write-Log -message $msg -logFile $logFile; throw $msg }
if (-not $groupName) { $msg = "Group name is required."; Write-Log -message $msg -logFile $logFile; throw $msg }
}
function Check-Connectivity {
param ([string]$domain, [string]$logFile)
Write-Log -message "Checking connectivity to domain controller for $domain." -logFile $logFile
try {
$ping = Test-Connection -ComputerName $domain -Count 1 -ErrorAction Stop
Write-Log -message "Successfully connected to domain controller for $domain." -logFile $logFile
return $true
} catch {
Write-Log -message "Unable to connect to domain controller for $domain." -logFile $logFile
Write-Log -message $_ -logFile $logFile
return $false
}
}
function Get-SourceUser {
param ([string]$username, [string]$domain, [pscredential]$cred, [string]$logFile)
Write-Log -message "Retrieving user $username from domain $domain." -logFile $logFile
try {
$user = Get-ADUser -Identity $username -Server $domain -Credential $cred -ErrorAction Stop
Write-Log -message "Successfully retrieved user $username from domain $domain." -logFile $logFile
return $user
} catch {
$msg = "Error retrieving user $username from domain $domain: $_"
Write-Log -message $msg -logFile $logFile
throw $msg
}
}
function Add-UserToTargetGroup {
param ([Microsoft.ActiveDirectory.Management.ADUser]$user, [string]$groupName, [string]$domain, [pscredential]$cred, [string]$logFile)
Write-Log -message "Adding user $($user.SamAccountName) to group $groupName in domain $domain." -logFile $logFile
try {
Add-ADGroupMember -Identity $groupName -Members $user.DistinguishedName -Server $domain -Credential $cred -ErrorAction Stop
$msg = "User $($user.SamAccountName) added to group $groupName successfully."
Write-Log -message $msg -logFile $logFile
Write-Output $msg
} catch {
$msg = "Error adding user $($user.SamAccountName) to group $groupName: $_"
Write-Log -message $msg -logFile $logFile
throw $msg
}
}
# Main Script Execution
try {
Import-Module ActiveDirectory -ErrorAction Stop
Write-Log -message "Active Directory module imported successfully." -logFile $logFile
Validate-Parameters -username $username -sourceDomain $sourceDomain -targetDomain $targetDomain -groupName $groupName -logFile $logFile
if (-not (Check-Connectivity -domain $sourceDomain -logFile $logFile)) {
throw "Source domain connectivity check failed."
}
if (-not (Check-Connectivity -domain $targetDomain -logFile $logFile)) {
throw "Target domain connectivity check failed."
}
$user = Get-SourceUser -username $username -domain $sourceDomain -cred $sourceCred -logFile $logFile
try {
Add-UserToTargetGroup -user $user -groupName $groupName -domain $targetDomain -cred $targetCred -logFile $logFile
} catch {
if ($groupName -notmatch "^(Domain Admins|Enterprise Admins|Schema Admins)$") {
$msg = "Cannot add user to group $groupName in domain $targetDomain. This may be a group-scope restriction — only Universal groups support true cross-domain membership."
Write-Log -message $msg -logFile $logFile
throw $msg
}
throw $_
}
} catch {
$errorMsg = "An error occurred: $_"
Write-Log -message $errorMsg -logFile $logFile
Write-Error $errorMsg
}
Example invocation:
.\AddUserToGroup.ps1 -username "user1" -sourceDomain "domain1.corp.local" -targetDomain "domain2.corp.local" -groupName "GroupName" -sourceCred (Get-Credential) -targetCred (Get-Credential) -logFile "C:\Scripts\ADUserAdd.log"
Note: I renamed a couple of the original helper functions (Get-User → Get-SourceUser, Add-UserToGroup → Add-UserToTargetGroup) since names like Get-User and Add-UserToGroup are generic enough to risk colliding with cmdlets from other modules loaded in the same session — a minor but worthwhile defensive habit in scripts meant to run unattended via MID Server.
Common Failure Scenarios and Resolutions
| Issue |
Resolution |
| Trust relationship missing |
Verify and establish the required trust using Active Directory Domains and Trusts. |
| Insufficient permissions |
Confirm the account has admin rights in both source and target domains. |
| Replication latency |
Recent changes in one domain may not have replicated yet — wait, or force replication with repadmin. |
| Network connectivity |
Confirm connectivity and open ports between domain controllers. |
| DNS misconfiguration |
Domain controllers need to resolve each other correctly — verify DNS settings. |
| Source user account issues |
Check whether the account is disabled or restricted in the source domain. |
| Group scope restriction |
Only Universal groups support true cross-domain membership — this is an AD design limitation, not a script bug. |
| PowerShell syntax/parameter errors |
Double-check the script for typos, especially if it was ever copied from a source that could have mangled special characters (see note below). |
A word of caution on copying PowerShell from web pages: if you're pasting a script from a blog, Word document, or rich-text source, watch for cmdlet names that have picked up unintended spaces around hyphens — New-ADGroup becoming New - ADGroup, for instance. This is a common artifact of copy-pasting from Word or certain CMS editors, and it silently breaks PowerShell syntax. Paste into a plain-text editor first and check cmdlet names carefully before running anything in production.