Public/Invoke-Workflow.ps1

function Invoke-Workflow {
    <#
    .SYNOPSIS
        Triggers a Nexthink Automation Workflow for devices and/or users.
 
    .DESCRIPTION
        Triggers the execution of a Workflow using external identifiers for
        devices and/or users.
 
        For users, each entry can specify one or more of:
          - sid : Security Identifier (SID)
          - upn : User Principal Name (email format)
          - uid : Globally unique user identifier
 
        For devices, each entry can specify one or more of:
          - collectorUid : Nexthink Collector UUID
          - name : Device name
          - uid : Globally unique device identifier
 
        This function accepts:
          - Simple strings (for common cases)
          - Rich objects/hashtables with explicit identifier properties
 
        Rules:
          - Up to 10,000 device entries
          - Up to 10,000 user entries
          - At least one of Devices or Users must be provided
 
        Examples:
          - Devices as plain collector UUIDs (string[])
          - Users as plain SIDs (string[])
          - Mixed/explicit identifiers using hashtables/PSCustomObjects
 
    .PARAMETER WorkflowId
        The NQL ID of the Automation Workflow to trigger.
 
        Must be a valid NQL identifier (e.g. "#workflow_example"), validated
        using Test-IsValidNqlId.
 
        Alias:
            NqlID
 
    .PARAMETER Devices
        Optional list of devices to target.
 
        Each entry MAY be:
          - A string:
              Treated as collectorUid (validated as a UUID)
          - A hashtable / PSCustomObject with any of:
              collectorUid : string, validated as UUID if present
              name : non-empty string
              uid : non-empty string (globally unique device ID)
 
        Constraints:
          - Up to 10,000 entries
          - At least one of collectorUid, name, uid must be present per entry
 
        Alias:
            DeviceIdList
 
        Examples:
            # Simple collector UIDs
            -Devices @(
                '3fa85f64-5717-4562-b3fc-2c963f66afa6',
                '0e8a9a54-9fd8-4e9a-83f4-4d611f9d1234'
            )
 
            # Explicit identifiers
            -Devices @(
                @{ collectorUid = '3fa85f64-5717-4562-b3fc-2c963f66afa6' },
                @{ name = 'LAPTOP-1234'; uid = 'dev-000123' }
            )
 
    .PARAMETER Users
        Optional list of users to target.
 
        Each entry MAY be:
          - A string:
              Treated as SID (validated with Test-IsValidSID)
          - A hashtable / PSCustomObject with any of:
              sid : string, validated as SID if present
              upn : string, validated as basic email-like UPN if present
              uid : non-empty string (globally unique user ID)
 
        Constraints:
          - Up to 10,000 entries
          - At least one of sid, upn, uid must be present per entry
 
        Alias:
            UserIdList
 
        Examples:
            # Simple SIDs
            -Users @(
                'S-1-5-21-1234567890-1234567890-1234567890-1001',
                'S-1-5-21-1234567890-1234567890-1234567890-1002'
            )
 
            # Explicit identifiers
            -Users @(
                @{ sid = 'S-1-5-21-1234-5678-9012-1001' },
                @{ upn = 'user1@contoso.com' },
                @{ uid = 'user-global-id-001'; upn = 'user2@contoso.com' }
            )
 
    .PARAMETER Parameters
        Optional key/value hashtable of workflow parameters.
 
        Keys:
          - Must be non-empty strings.
 
        Values:
          - string, int, bool
          - or arrays of string/int/bool
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        [object]
 
        Returns the response object from Invoke-NxtApi, which typically includes
        a requestUuid representing the workflow execution request.
 
    .EXAMPLE
        Invoke-Workflow `
            -WorkflowId '#workflow_example' `
            -Devices @('3fa85f64-5717-4562-b3fc-2c963f66afa6')
 
    .EXAMPLE
        Invoke-Workflow `
            -WorkflowId '#workflow_example' `
            -Users @(
                'S-1-5-21-1234567890-1234567890-1234567890-1001',
                @{ upn = 'user1@contoso.com' }
            )
 
    .EXAMPLE
        $devices = @(
            @{ collectorUid = '3fa85f64-5717-4562-b3fc-2c963f66afa6' }
            @{ name = 'LAPTOP-1234' }
        )
 
        $users = @(
            @{ sid = 'S-1-5-21-1234567890-1234567890-1234567890-1001' }
            @{ upn = 'user1@contoso.com'; uid = 'user-global-001' }
        )
 
        $params = @{
            reason = 'Standard workflow run'
            priority = 1
        }
 
        Invoke-Workflow `
            -WorkflowId '#complex_workflow' `
            -Devices $devices `
            -Users $users `
            -Parameters $params
 
    #>

    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter(
            Mandatory = $true,
            Position  = 0
        )]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            if (-not (Test-IsValidNqlId $_)) { throw "Invalid NQL Query ID: $_" }
            $true
        })]
        [Alias('NqlID')]
        [string]$WorkflowId,

        [Parameter(
            Mandatory = $false,
            Position  = 1
        )]
        [Alias('DeviceIdList')]
        [ValidateScript({
            if ($null -eq $_) { return $true }

            if ($_.Count -gt 10000) { throw "Devices cannot contain more than 10,000 entries." }

            foreach ($entry in $_) {
                # Simple string -> collectorUid
                if ($entry -is [string]) {
                    $guid = [Guid]::Empty
                    if (-not [Guid]::TryParse($entry, [ref]$guid)) {
                        throw "Device value '$entry' is a string and is treated as collectorUid, but it is not a valid UUID."
                    }
                    continue
                }

                if (-not ($entry -is [hashtable] -or $entry -is [psobject])) {
                    throw "Each device entry must be a string, hashtable, or PSCustomObject. Got: [$($entry.GetType().FullName)]."
                }

                $collectorUid = $null
                $name         = $null
                $uid          = $null

                if ($entry.PSObject.Properties['collectorUid']) {
                    $collectorUid = [string]$entry.collectorUid
                }
                if ($entry.PSObject.Properties['name']) {
                    $name = [string]$entry.name
                }
                if ($entry.PSObject.Properties['uid']) {
                    $uid = [string]$entry.uid
                }

                if (-not $collectorUid -and -not $name -and -not $uid) {
                    throw "Each device entry must specify at least one of: collectorUid, name, uid."
                }

                if ($collectorUid) {
                    $guid = [Guid]::Empty
                    if (-not [Guid]::TryParse($collectorUid, [ref]$guid)) {
                        throw "collectorUid '$collectorUid' is not a valid UUID."
                    }
                }

                if ($name -and [string]::IsNullOrWhiteSpace($name)) {
                    throw "Device name cannot be empty or whitespace."
                }

                if ($uid -and [string]::IsNullOrWhiteSpace($uid)) {
                    throw "Device uid cannot be empty or whitespace."
                }
            }

            $true
        })]
        [object[]]$Devices,

        [Parameter(
            Mandatory = $false,
            Position  = 2
        )]
        [Alias('UserIdList')]
        [ValidateScript({
            if ($null -eq $_) { return $true }
            if ($_.Count -gt 10000) { throw "Users cannot contain more than 10,000 entries." }

            foreach ($entry in $_) {
                # Simple string -> SID
                if ($entry -is [string]) {
                    if (-not (Test-IsValidSID $entry)) {
                        throw "User value '$entry' is a string and is treated as SID, but it is not a valid security identifier."
                    }
                    continue
                }

                if (-not ($entry -is [hashtable] -or $entry -is [psobject])) {
                    throw "Each user entry must be a string, hashtable, or PSCustomObject. Got: [$($entry.GetType().FullName)]."
                }

                $sid = $null
                $upn = $null
                $uid = $null

                if ($entry.PSObject.Properties['sid']) { $sid = [string]$entry.sid }
                if ($entry.PSObject.Properties['upn']) { $upn = [string]$entry.upn }
                if ($entry.PSObject.Properties['uid']) { $uid = [string]$entry.uid }

                if (-not $sid -and -not $upn -and -not $uid) { throw "Each user entry must specify at least one of: sid, upn, uid." }
                if ($sid -and -not (Test-IsValidSID $sid)) { throw "sid '$sid' is not a valid security identifier." }
                if ($upn -and $upn -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') { throw "upn '$upn' does not appear to be a valid UPN/email format." }
                if ($uid -and [string]::IsNullOrWhiteSpace($uid)) { throw "User uid cannot be empty or whitespace." }
            }

            $true
        })]
        [object[]]$Users,

        [Parameter(Mandatory = $false)]
        [ValidateScript({
            if ($null -eq $_ -or $_.Count -eq 0) { return $true }

            foreach ($key in $_.Keys) {
                if (-not ($key -is [string]) -or [string]::IsNullOrWhiteSpace($key)) {
                    throw "All workflow parameter names must be non-empty strings. Invalid key: '$key'"
                }
            }

            foreach ($value in $_.Values) {
                if ($null -eq $value) { continue }

                if ($value -is [array]) {
                    foreach ($item in $value) {
                        if ($null -eq $item) { continue }
                        if (-not ($item -is [string] -or $item -is [int] -or $item -is [bool])) {
                            throw "Workflow parameter array values must be string, int, or bool. Got: [$($item.GetType().FullName)]."
                        }
                    }
                }
                elseif (-not ($value -is [string] -or $value -is [int] -or $value -is [bool])) {
                    throw "Workflow parameter values must be string, int, or bool (or arrays of these). Got: [$($value.GetType().FullName)]."
                }
            }

            $true
        })]
        [hashtable]$Parameters
    )

    $apiType = 'WF_Exec'

    # Ensure at least one target collection is provided
    if ((-not $Devices -or $Devices.Count -eq 0) -and
        (-not $Users   -or $Users.Count   -eq 0)) {

        $message = "You must specify at least one device or one user. Both parameters cannot be empty."
        Write-CustomLog -Message $message -Severity 'ERROR'
        throw $message
    }

    # ----------------------------------------------------------------
    # Build request body for new external-identifier model
    # ----------------------------------------------------------------
    $body = @{
        workflowId = $WorkflowId
    }

    if ($Devices -and $Devices.Count -gt 0) {
        $body.devices = foreach ($entry in $Devices) {
            if ($entry -is [string]) {
                # String → collectorUid
                [PSCustomObject]@{ collectorUid = $entry }
            }
            else {
                $deviceObj = [ordered]@{}
                if ($entry.PSObject.Properties['collectorUid'] -and $entry.collectorUid) {
                    $deviceObj.collectorUid = [string]$entry.collectorUid
                }
                if ($entry.PSObject.Properties['name'] -and $entry.name) {
                    $deviceObj.name = [string]$entry.name
                }
                if ($entry.PSObject.Properties['uid'] -and $entry.uid) {
                    $deviceObj.uid = [string]$entry.uid
                }
                [PSCustomObject]$deviceObj
            }
        }
    }

    if ($Users -and $Users.Count -gt 0) {
        $body.users = foreach ($entry in $Users) {
            if ($entry -is [string]) {
                # String → SID
                [PSCustomObject]@{ sid = $entry }
            }
            else {
                $userObj = [ordered]@{}
                if ($entry.PSObject.Properties['sid'] -and $entry.sid) {
                    $userObj.sid = [string]$entry.sid
                }
                if ($entry.PSObject.Properties['upn'] -and $entry.upn) {
                    $userObj.upn = [string]$entry.upn
                }
                if ($entry.PSObject.Properties['uid'] -and $entry.uid) {
                    $userObj.uid = [string]$entry.uid
                }
                [PSCustomObject]$userObj
            }
        }
    }

    if ($Parameters -and $Parameters.Count -gt 0) {
        $body.params = $Parameters
    }

    $bodyJson = $body | ConvertTo-Json -Depth 6

    Write-CustomLog -Message (
        "Invoking Workflow (external IDs). WorkflowId='{0}', Devices={1}, Users={2}, HasParameters={3}" -f `
            $WorkflowId,
            ($Devices -join ','),
            ($Users   -join ','),
            [bool]($Parameters -and $Parameters.Count -gt 0)
    ) -Severity 'DEBUG'

    return Invoke-NxtApi -Type $apiType -Body $bodyJson -ReturnResponse
}