Set EmergencyOffboarding attribute to Yes in case of Disable on Active lifecycle state within iqservice powershell script

:bangbang: Please be sure you’ve read the docs and API specs before asking for help. Also, please be sure you’ve searched the forum for your answer before you create a new topic.

Please consider addressing the following when creating your topic:

  • What have you tried?

We currently make of an ConnectorAfterModify to execute an powershell script stored on our IQService to perform specific actions towards the EXO mailbox and Entra ID account in case we Disable the account on our Entra ID / EXO source, as disable account sources action configured on our leaver/left lifecycle states.

This solution is working as desired, however, in some cases we have an emergency offboarding at which, in order to speed up things here, in the Active lifecycle state we directly disable the account on all applicable sources. In such a scenario, we do not want this powershell script to be executed. Based on https://developer.sailpoint.com/discuss/t/using-before-provision-rule-for-a-delete-operation/88951/9 I therefore attached to our EXO source:

[
    {
        "op": "add",
        "path": "/connectorAttributes/cloudServicesIDNSetup",
        "value": {
            "eventConfigurations": [
                {
                    "eventActions": [
                        {
                            "Action": "AddArgument",
                            "Attribute": "EmergencyOffboarding",
                            "Value": "yes"
                        }
                    ],
                    "Identity Attribute Triggers": [
                        {
                            "Attribute": "cloudLifecycleState",
                            "Value": "active",
                            "Operation": "eq"
                        }
                    ],
                    "Operation": "Disable"
                }
            ]
        }
    }
]

At which I was now expecting that within the iqservice powershell script, in case of Disable account on active lifecycle state, you would get value Yes within $requestObject.Attributes.EmergencyOffboarding however $requestObject.Attributes is empty.

  • What errors did you face (share screenshots)?

None

  • Share the details of your efforts (code / search query, workflow json etc.)?

See above

  • What is the result you are getting and what were you expecting?

In case of Disable account on active lifecycle state, you would get value Yes within $requestObject.Attributes.EmergencyOffboarding as part of the IQService powershell script attached to the source via connector rule

Hey Steven,

You would have to filter in your AfterModify rule to check if LCS = Active.

In other words some filter to check if

operation = modify/disable

attribute = lcsAttr → active

== Do nothing.

(If i understand your case correctly)

You can do this either by provisioning LCS value to an attribute in AD/Entra (Maybe you do this already) and use this attribute to check against when AfterModify triggers.

You could also do the filtering on the server-side after the AfterModify script as executed to check if account is disabled natively (userAccountControl in AD or accountEnabled in Entra).

Hi Sebastian,

How do I achieve this one within the AfterModify powershell script? Currently the request object as I’m getting is empty for attributes:

2026-01-27 18:11:06 - [INFO] Request object contents:
2026-01-27 18:11:06 -

Attributes : {}
AttributeRequests : {}
PermissionRequests : {}
NativeIdentity :
Instance :
ApplicationName : Entra ID / EXO ACC [source]
Operation : Disable
ObjectType :
ProvisioningResult : sailpoint.Utils.objects.ProvisioningResult

We don’t want to check if account is disabled natively. As part of emergency offboarding, our current working procedure is that from within identity security we directly disable the account on applicable sources, we don’t do this one within for example entra id directly.

What you’re seeing is expected in the sense that the IQService AccountRequest for Disable often arrives with an empty metadata map ($requestObject.Attributes) and no AttributeRequests, so there’s nothing in PowerShell to branch on. In other words, your AddArgument config isn’t being carried through into the IQService request for this operation.

The reliable way to solve this is to do the filtering in the ConnectorAfterModify rule itself (on the VA), before triggering any downstream PowerShell actions:

  • If Operation == Disable and identity cloudLifecycleState == active (your emergency offboarding path), then skip the script execution.

  • Otherwise run your current script as normal.

That way you don’t depend on $requestObject.Attributes being populated for Disable. SailPoint’s own guidance for these rules is to keep the rule logic minimal and use it to control whether you invoke the external script.

If you must branch inside PowerShell, the workaround is to provision a real “flag” attribute to the target (e.g., an extensionAttribute) as part of the emergency workflow and check for that in the IQService script — but the simplest/cleanest is the rule-side filter.

If you want you can send a snippet of your afterModify logic and we can take a look.

Thanks for your detailed response @Swegmann, excluding active lifecyclestate within our powershell invoke sounds like our desired fix, however, how do I retrieve the cloudLifecycleState within an ConnectorAfterModify rule?

I tried to modify it towards

## Do not call our modify script in case of our current lifecyclestate is active
$cls = $resultObject.Attributes.cloudLifecycleState
If ($cls -eq \"active\") {
	# Do nothing
} Else {
	$commandWithArgs = \"$command -requestString '$requestAsString'\"
	Invoke-Expression $commandWithArgs
}

However, this seems to take no effect

What you can try is to pass cloudLifecycleState into the request as an AccountRequest argument, then read it using request.getArguments().get("cloudLifecycleState") and skip the invoke when it’s Disable + active.

Something like this:

import sailpoint.object.Attributes;

Attributes args = request.getArguments();
String cls = null;

if (args != null) {
  Object v = args.get("cloudLifecycleState");
  if (v != null) cls = v.toString();
}

log.info("cloudLifecycleState from AccountRequest arguments = " + cls);

if ("active".equalsIgnoreCase(cls) && request.getOperation() != null
    && "Disable".equalsIgnoreCase(request.getOperation().toString())) {

  log.info("Skipping IQService call: Disable + cloudLifecycleState=active");
  return result; // do nothing; do NOT invoke powershell
}

// else proceed with your normal invoke logic...
String commandWithArgs = command + " -requestString '" + requestAsString + "'";
connector.common.Util.runCommand(commandWithArgs);   // (or your existing invocation method)
return result;

Sorry @Swegmann I’m not sure I follow this one, our current ConnectorAfterModify is powershell based, not java and is based on Before and after operations on source account Rule | SailPoint Developer Community

{
    "description": "Run AfterModify script using IQService",
    "type": "ConnectorAfterModify",
    "signature": {
        "input": [],
        "output": null
    },
    "sourceCode": {
        "version": "1.0",
        "script": "$logDate = Get-Date -UFormat %Y%m%d\n$logFile = \"D:\\IQService\\Scripts\\Logs\\AfterModifyEntraID_EXO_$logDate.log\"\n$command = \"D:\\IQService\\Scripts\\AfterModifyEntraID_EXO.ps1\"\n$enableDebug = $true\n\n#====================-------Helper functions-------====================\nfunction LogToFile([String] $info) {\n    $info | Out-File $logFile -Append\n}\n    \n#====================-------Get the request object-------====================\nTry{\n    if($enableDebug) {\n        LogToFile(\"Entering SailPoint rule\")\n    }\n    Add-type -path \"D:\\IQService\\Utils.dll\";\n    $sReader = New-Object System.IO.StringReader([System.String]$env:Request);\n    $xmlReader = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sReader));\n    $requestObject = New-Object Sailpoint.Utils.objects.AccountRequest($xmlReader);\n    $requestAsString = $env:Request\n    if($enableDebug) {\n        LogToFile(\"Request as XML object is: $requestAsString\")\n    }\n    \n    $sResult = New-Object System.IO.StringReader([System.String]$env:Result);\n    $xmlReader_Result = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sResult));\n\n    $resultObject = New-Object Sailpoint.Utils.objects.ServiceResult($xmlReader_Result);\n\n    if ( $resultObject.Errors.count -gt 0 ){\n        LogToFile(\"ERROR in previous modify account action:\");\n        LogToFile($resultObject.Errors);\n        throw $resultObject.Errors;\n    }\n\n    LogToFile(\n    ($resultObject | Format-List * | Out-String)\n    )\n    #Call the client script\n    $commandWithArgs = \"$command -requestString '$requestAsString'\"\n\tInvoke-Expression $commandWithArgs\n\t\n}\nCatch {\n    $ErrorMessage = $_.Exception.Message \n    $ErrorItem = $_.Exception.ItemName\n    LogToFile(\"Error: Item = $ErrorItem -> Message = $ErrorMessage\")\n    throw $ErrorMessage\n}\nif($enableDebug) {\n    LogToFile(\"Exiting SailPoint rule\")\n}\n\n        "
    },
    "attributes": {
        "ObjectOrientedScript": "true",
        "extension": ".ps1",
        "sourceVersion": "1.0",
        "disabled": "false",
        "program": "powershell.exe",
        "timeout": "300"
    },
    "id": "<<GUID>>",
    "name": "Entra ID / EXO AfterModify Rule",
    "created": "2025-09-11T07:54:18.537Z",
    "modified": "2026-01-28T12:40:17.321Z"
}

Looking at our log file $env:request currently only exists of

Request as XML object is:
<AccountRequest application="Entra ID / EXO ACC [source]" op="Disable" nativeIdentity="<guid>">
<ProvisioningResult status="committed" />
</AccountRequest>

Sorry, of course its PS.

I’m beginning to wonder if it might be difficult to go for the “Disable” operation itself as you get no attributeRequests.

An idea is to include something detectable, e.g.:

  • set an AD/Azure attribute like extensionAttribute15 = "EmergencyOffboarding" (or similar)

  • this creates a Modify request with <AttributeRequest> entries

  • then AfterModify can read those and decide whether to run the downstream script

This is the “provision a flag” approach (not necessarily LCS itself). It’s ugly, but it works. You would instead filter the afterModify powershell rule on a modification event rather than a disable operation.

Another idea would be to leverage Workflow + GraphAPI httpRequest to do your Emergency off-boarding, bypassing a “disable” event in ISC, but you would loose traceability in ISC.

Example rule:

$logDate = Get-Date -UFormat %Y%m%d
$logFile = "D:\IQService\Scripts\Logs\AfterModifyEntraID_EXO_$logDate.log"
$command = "D:\IQService\Scripts\AfterModifyEntraID_EXO.ps1"
$enableDebug = $true

# --- Filter settings ---
$filterAttributeName = "EmergencyOffboarding"
$filterAttributeTrueValues = @("yes", "true", "1")  # case-insensitive compare

#====================-------Helper functions-------====================
function LogToFile([String] $info) {
    $info | Out-File $logFile -Append
}

function ToLowerSafe([object] $v) {
    if ($null -eq $v) { return $null }
    return ([string]$v).Trim().ToLowerInvariant()
}

function Get-RequestOperation([object] $req) {
    try {
        if ($null -ne $req -and $null -ne $req.Operation) { return [string]$req.Operation }
    } catch {}
    return $null
}

function Get-AttributeRequests([object] $req) {
    # Best-effort: different IQService/Utils.dll builds expose this slightly differently.
    try {
        if ($null -ne $req -and $null -ne $req.AttributeRequests) { return $req.AttributeRequests }
    } catch {}
    return $null
}

function Has-ModifyAttributeFlag {
    param(
        [Parameter(Mandatory=$true)] $req,
        [Parameter(Mandatory=$true)] [string] $attrName,
        [Parameter(Mandatory=$true)] [string[]] $trueValuesLower
    )

    $attrReqs = Get-AttributeRequests -req $req
    if ($null -eq $attrReqs) {
        LogToFile("No AttributeRequests collection found on request object.")
        return $false
    }

    foreach ($ar in $attrReqs) {
        # Common fields: Name + Value. Some versions use AttributeName.
        $name = $null
        $value = $null

        try { if ($null -ne $ar.Name) { $name = [string]$ar.Name } } catch {}
        if ($null -eq $name) {
            try { if ($null -ne $ar.AttributeName) { $name = [string]$ar.AttributeName } } catch {}
        }

        try { if ($null -ne $ar.Value) { $value = $ar.Value } } catch {}

        $nameNorm = ToLowerSafe $name
        if ($nameNorm -ne (ToLowerSafe $attrName)) { continue }

        # Value can be string or array; normalize to strings
        if ($value -is [System.Array]) {
            foreach ($v in $value) {
                $vNorm = ToLowerSafe $v
                if ($null -ne $vNorm -and $trueValuesLower -contains $vNorm) {
                    LogToFile("Matched AttributeRequest: $attrName = '$v'")
                    return $true
                }
            }
        } else {
            $vNorm = ToLowerSafe $value
            if ($null -ne $vNorm -and $trueValuesLower -contains $vNorm) {
                LogToFile("Matched AttributeRequest: $attrName = '$value'")
                return $true
            }
        }

        LogToFile("Found AttributeRequest for '$attrName' but value did not match true values. Value='$value'")
        return $false
    }

    LogToFile("No AttributeRequest found for attribute '$attrName'.")
    return $false
}

#====================-------Get the request object-------====================
Try {
    if ($enableDebug) { LogToFile("Entering SailPoint rule") }

    Add-type -path "D:\IQService\Utils.dll"

    $sReader = New-Object System.IO.StringReader([System.String]$env:Request)
    $xmlReader = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sReader))
    $requestObject = New-Object Sailpoint.Utils.objects.AccountRequest($xmlReader)
    $requestAsString = $env:Request

    if ($enableDebug) { LogToFile("Request XML: $requestAsString") }

    $sResult = New-Object System.IO.StringReader([System.String]$env:Result)
    $xmlReader_Result = [System.xml.XmlTextReader]([sailpoint.utils.xml.XmlUtil]::getReader($sResult))
    $resultObject = New-Object Sailpoint.Utils.objects.ServiceResult($xmlReader_Result)

    if ($resultObject.Errors.count -gt 0) {
        LogToFile("ERROR in previous modify account action:")
        LogToFile($resultObject.Errors)
        throw $resultObject.Errors
    }

    if ($enableDebug) {
        LogToFile(($resultObject | Format-List * | Out-String))
    }

    #====================-------Filtering logic-------====================
    $op = Get-RequestOperation -req $requestObject
    $opNorm = ToLowerSafe $op
    LogToFile("Request.Operation = '$op'")

    # 1) Only run on Modify
    if ($opNorm -ne "modify") {
        LogToFile("Skipping invoke: operation is not Modify.")
        return
    }

    # 2) Only run when Modify includes EmergencyOffboarding=yes/true/1
    $trueValuesLower = @()
    foreach ($tv in $filterAttributeTrueValues) { $trueValuesLower += (ToLowerSafe $tv) }

    if (-not (Has-ModifyAttributeFlag -req $requestObject -attrName $filterAttributeName -trueValuesLower $trueValuesLower)) {
        LogToFile("Skipping invoke: Modify did not include $filterAttributeName with a true value.")
        return
    }

    #====================-------Call the client script-------====================
    $commandWithArgs = "$command -requestString '$requestAsString'"
    LogToFile("Invoking: $commandWithArgs")
    Invoke-Expression $commandWithArgs
}
Catch {
    $ErrorMessage = $_.Exception.Message
    $ErrorItem = $_.Exception.ItemName
    LogToFile("Error: Item = $ErrorItem -> Message = $ErrorMessage")
    throw $ErrorMessage
}

if ($enableDebug) { LogToFile("Exiting SailPoint rule") }

Thanks a lot for your feedback @Swegmann , I’ve discussed this one internally and we decided to resolve this one by calling the search api of idsec within our exo script to fetch the desired lifecyclestate, we retrieve the clientsecret securely by getting the value from our password vault solution.