Workflow Loop error- RemoveAccess not working inside loop

I created a workflow (shown below) to remove access upon an identity becoming inactive. I am able to successfully remove the access in a workflow when I do it outside of a loop, but when I try it inside of the loop I get this error:

task failed: activity error (type: sp:access:manage, scheduledEventID: 17, startedEventID: 18, identity: 1@sp-workflow-worker-stg-us-east-1-7bd7d5469f-5lbdg@sp-workflow-engine): request failed (type: Bad Request, retryable: false): 400 - 400 Bad Request: (type: HTTP Response Returned a Client Error, retryable: false): request failed (type: Bad Request, retryable: false): 400 - 400 Bad Request (type: fundamental, retryable: true)

From my debugging I have determined that it is coming from the RemoveAccess action within the loop. When I try to use the RemoveAccess action outside of the loop with the same input (same user ID and same item info from GetAccess), it is successful.

I have seen this same error in a few other threads but I’m not able to find any explanation or resolution. Any help is appreciated, thank you.

Workflow:

{
    "id": "XXXXXXXXXXXXXXXXXXXXXXXXX",
    "name": "Remove Subtemplate Upon Becoming Inactive",
    "description": "",
    "created": "2025-11-04T16:07:26.774088018Z",
    "modified": "2025-11-10T15:26:30.824910248Z",
    "modifiedBy": {
        "type": "IDENTITY",
        "id": "XXXXXXXXXXXXXXXXXXXXXXXXX",
        "name": "My.Name"
    },
    "definition": {
        "start": "Compare Strings",
        "steps": {
            "Compare Strings": {
                "actionId": "sp:compare-strings",
                "choiceList": [
                    {
                        "comparator": "StringEquals",
                        "nextStep": "Get Accounts",
                        "variableA.$": "$.trigger.newState.name",
                        "variableB": "inactive"
                    }
                ],
                "defaultStep": "End Step - Success",
                "description": "Verify that new lifecycle state is inactive.",
                "displayName": "Verify new state is inactive.",
                "type": "choice"
            },
            "End Step - Success": {
                "actionId": "sp:operator-success",
                "displayName": "New lifecycle state not inactive.",
                "type": "success"
            },
            "End Step - Success 1": {
                "actionId": "sp:operator-success",
                "displayName": "Identity does not have an EMP account.",
                "type": "success"
            },
            "End Step - Success 3": {
                "actionId": "sp:operator-success",
                "displayName": "Identity does not have subtemplates.",
                "type": "success"
            },
            "End Step - Success 4": {
                "actionId": "sp:operator-success",
                "displayName": "",
                "type": "success"
            },
            "Get Accounts": {
                "actionId": "sp:get-accounts",
                "attributes": {
                    "getAccountsBy": "specificIdentity",
                    "identity.$": "$.trigger.identity.id"
                },
                "displayName": "",
                "nextStep": "Verify Data Type",
                "type": "action",
                "versionNumber": 1
            },
            "Loop": {
                "actionId": "sp:loop:iterator",
                "attributes": {
                    "context.$": "$.trigger.identity.id",
                    "input.$": "$.getAccounts.accounts[?(@.sourceId==\"XXXXXXXXXXXXXXXXXXX\")].attributes.LinkedSubtemplateIDs",
                    "start": "Get Access",
                    "steps": {
                        "End Step - Success 2": {
                            "actionId": "sp:operator-success",
                            "displayName": "",
                            "type": "success"
                        },
                        "Get Access": {
                            "actionId": "sp:access:get",
                            "attributes": {
                                "accessprofiles": true,
                                "entitlements": true,
                                "getAccessBy": "searchQuery",
                                "query.$": "$.loop.loopInput",
                                "roles": true
                            },
                            "displayName": "",
                            "nextStep": "Send Email",
                            "type": "action",
                            "versionNumber": 1
                        },
                        "Manage Access": {
                            "actionId": "sp:access:manage",
                            "attributes": {
                                "comments": "Removing subtemplates upon identity becoming inactive. ",
                                "removeDuration": null,
                                "removeIdentity.$": "$.loop.context",
                                "requestType": "REVOKE_ACCESS",
                                "requestedItems.$": "$.getAccess.accessItems"
                            },
                            "displayName": "",
                            "nextStep": "End Step - Success 2",
                            "type": "action",
                            "versionNumber": 1
                        },
                        "Send Email": {
                            "actionId": "sp:send-email",
                            "attributes": {
                                "body.$": "$.getAccess.accessItems",
                                "context": {},
                                "recipientEmailList": [
                                    "XXXXXXXXXXXXXXXXXXXXXXXXX"
                                ],
                                "replyTo": null,
                                "subject.$": "$.loop.context"
                            },
                            "displayName": "",
                            "nextStep": "Manage Access",
                            "type": "action",
                            "versionNumber": 2
                        }
                    }
                },
                "displayName": "",
                "nextStep": "End Step - Success 4",
                "type": "action",
                "versionNumber": 1
            },
            "Verify Data Type": {
                "actionId": "sp:compare-unary",
                "choiceList": [
                    {
                        "comparator": "IsPresent",
                        "nextStep": "Verify Data Type 1",
                        "variableA.$": "$.getAccounts.accounts[?(@.sourceId==\"XXXXXXXXXXXXXXXXXXX\")].id"
                    }
                ],
                "defaultStep": "End Step - Success 1",
                "displayName": "Verify identity has EMP account.",
                "type": "choice"
            },
            "Verify Data Type 1": {
                "actionId": "sp:compare-unary",
                "choiceList": [
                    {
                        "comparator": "IsNull",
                        "nextStep": "End Step - Success 3",
                        "variableA.$": "$.getAccounts.accounts[?(@.sourceId==\"XXXXXXXXXXXXXXXXXXX\")].attributes.LinkedSubtemplateIDs"
                    }
                ],
                "defaultStep": "Loop",
                "description": "Check to see if LinkedSubTemplates value is null. If False, Identity has subtemplates",
                "displayName": "Check for subtemplates",
                "type": "choice"
            }
        }
    },
    "enabled": false,
    "executionCount": 0,
    "failureCount": 0,
    "creator": {
        "type": "IDENTITY",
        "id": "XXXXXXXXXXXXXXXXXXXXXXXXX",
        "name": "My.Name"
    },
    "owner": {
        "type": "IDENTITY",
        "id": "XXXXXXXXXXXXXXXXXXXXXXXXX",
        "name": "My.Name"
    },
    "trigger": {
        "type": "EVENT",
        "attributes": {
            "id": "idn:identity-lifecycle-state-change-processed"
        }
    }
}```

Hi Sarah,

Just out of curiosity, does the “remove all access” option in the identity profile not work for you? It would be a better and more efficient way to remove access when the identity becomes inactive.

Unfortunately, that doesn’t work here since there are other entitlements they need to retain.

In this case, I suggest you to use a certification campaign in the workflow to remove all access revoking the certification automatically, so you can use a filter to exclude the entitlements they need to retain from the campaign.

More detailed steps here: Remove All Access Workflow

The loop might work, but it has a limit of one hundred items per iteration, so it can get complicated if an identity has more than 100 access items.

I’m not sure that this solution would be right for me either…. to better explain the situation, I want to remove all entitlements of a certain type, and ignore all other access. The way I thought to go about doing so is by pulling the associated entitlement attribute in the source account which lists all entitlements of that type, then loop through those entitlements to remove them one by one.

As I mentioned above, it works to do it this way when I only remove an entitlement outside of the loop, but when I try to remove even a single access item in a loop it gives the 400 error. I see you said that it can get complicated if an identity has more than 100 access items, but this does not apply here.

It seems to me that there is a bug within how the RemoveAccess functions within a loop.

Hi Sarah,

Can you try changing your context to $.trigger and subsequent calls to the context, take $.loop.context.identity.id

For your Get Access step, try "query.$": "$.loop.loopInput.<id>", where is the path in the json that has the names/values of the roles / entitlements / access profiles.

I find most often if a loop is returning an error, it’s because the input or the context (or usage of one of these) isn’t correct

Hi Margo,

I will try making these changes, but I believe that the input and context are both reading correctly.

I checked this by using the Send Email action and sending myself an email with the output from Get Access in the body and the identity id as the subject. I also did this outside of a loop so I could make sure that the information all matched up correctly.

Add Filter before Manage Access

"$.getAccess.accessItems[?(@.id != null)]"

inside loop $.getAccess.accessItems refers to loops internal execution context

-Mahesh

Hi Mahesh,

I’m not sure how the filter would help here- I want to use the internal execution inside the loop.

In my loop, the input is the entitlement name, with the identity id as context. I take the input (entitlement name) and search for it using Get Access (“query.$”: “$.loop.loopInput”). Then I send an email with the information to verify it is correct. Then i use Manage Access to remove the entitlement.

I did try adding this filter just to try though, and it gave the same error.

Hi @claysar2

Are these entitlements from a flat file source or a direct-connect source? Did you check the account activity and/or event logs to check for more details?

I had experienced the same error in one of my workflow, similar to yours. After reviewing I found the workflow failed to create a Jira ticket because there’s was already another pending ticket about the same access to be revoked for that specific user.

Hello Noor,

These entitlements are from a direct-connect source: Epic. I checked the account activity but there are no relevant logs relating to the access management.

I thought at one point that it may be ticket related, but have ruled that out. Because I am able to remove the entitlements without error outside of the loop it leads me to believe that the problem is due to the loop functionality.

Can you please try this?

Use the ‘Get Access’ action outside the loop i.e after ‘Check for subtemplates’ operator and tick/enable only the ‘Get Access Profiles’ option

  • Access Selection Method: By Identity
  • By Identity: $.trigger.identity.id

Let’s say the access profiles’ names related to EMP account are;
EMP Test Access …..
EMP - Test Access ….

Loop Input: $.getAccess.accessItems[?(@.type == “ACCESS_PROFILE”) && (@.name =~ /EMP.*/i|| @.name =~ /EMP.*._.*.Test.*/i)]
Loop Context: $.trigger.identity

Manage Access (inside the loop):
-Identity: $.loop.context.id
-Request Type: Remove Access
-Access to Manage: $.loop.loopInput
-Comments: test comments

Let me know if this helps.

If i am understanding correctly, you are suggesting that I loop through the access profiles that are listed on the account after finding them with Get Access.

This does not work for this scenario, as I do not want to remove all access from the identity.

Hi Sarah,

Could you please describe which specific access do you want to remove from the inactive identity?

Is it to revoke all their access (entitlements/access profiles/roles) if they do have EMP account setup? OR

Is it to revoke some of their specific access (entitlements/access profiles/roles) if they do have EMP account setup?

FYI, revoke requests for individual entitlements are limited to one entitlement per access request.

Hi Noor,

I want to only revoke some of their access if they have an EMP account set up.

in the EMP (Epic) source, the entitlements I want to remove are the ones that have the type: linkedSubTemplate. There is an associated entitlement attribute “LinkedSubTemplateIDs” on the EMP account, which holds the names of the entitlements. This entitlement attribute is what I am using as the input for the loop.

I have checked that when I search using this input in the loop I get the correct mapped values from Get Access, but get the 400 error from Manage Access.

Hi Sarah,

Can you please try this workflow in your sandbox environment and then share your test result? You will need to update the workflow as per your scenario/details.

Workflow JSON is attached.

RevokeSpecificEntitlementsbelongstoSpecificSource20251112.json (4.1 KB)

1 Like

Hello Noor,

I was able to make the workflow you provided work for me with a little bit of customizing. I have shared my final workflow JSON here in case it is helpful for others facing similar issues in the future.

Thank you so much for your help!

revoke_entitlements_workflow.json (7.0 KB)

1 Like

Hi Sarah,

You are welcome! I am glad that this has worked for your use case.

If you want, you can mark this post as resolved.

Thanks