ISC Extended Workflow Helper


:spiral_notepad: Description isc-extended-workflow-helper extends SailPoint Identity Security workflows by providing an HTTP API to run Java/BeanShell code. It enables workflows to execute custom logic, integrate with external systems, or perform advanced transformations that go beyond its out-of-the-box capabilities.
:balance_scale: Legal Agreement By using this CoLab item, you are agreeing to SailPoint’s Terms of Service for our developer community and open-source CoLab.
:hammer_and_wrench: Repository Link GitHub - sailpoint-oss/colab-isc-extended-workflow-helper · GitHub
:hospital: Supported by Community Developed

Overview

The project focuses on extending the capabilities of SailPoint Identity Security workflows by integrating with them as an API. It allows to execute the arbitrary Java/Beanshell code using the provided endpoints. The result of the Beanshell code can be collected in any data type depending on the use case.

Requirements

  • Java JDK 17
  • Maven 3.8+

Guide

Installing Locally

Install Dependencies
mvn clean install -Dmaven.test.skip=true

Start the Server with API Calling Support Enabled:
mvn spring-boot:run -DskipTests -Dspring-boot.run.jvmArguments="--add-opens java.base/sun.net.www.protocol.https=ALL-UNNAMED"

Start the Server with API Calling Support Disabled:
mvn exec:java

Available Endpoints

  • /api/scripts
  • /api/escape
  • /api/upload-jars

Returning data

1. Type: String ("")


  {
    "language": "beanshell",
    "script": "return \"Hello from BeanShell!\";"
  }

Expected Output:

"data": "Hello from BeanShell!"

2. Type: List ([])


  {
    "language": "beanshell",
    "script": "import java.util.*; return Arrays.asList(\"apple\", \"banana\", \"cherry\");"
  }

Expected Output:

"data": ["apple", "banana", "cherry"]

3. Type: Map ({})


  {
    "language": "beanshell",
    "script": "import java.util.*; Map<String,Object> map=new LinkedHashMap<>(); map.put(\"key\",\"value\"); map.put(\"status\",\"ok\"); return map;"
  }

Expected Output:

"data": {
    "key": "value",
    "status": "ok"
}

4. Printed Output Only


  {
    "language": "beanshell",
    "script": "System.out.println(\"Hello World\"); System.out.println(\"Generated Password: 12345\");"
  }

Expected Output:

"data": {
    "output": "Hello World\nGenerated Password: 12345"
}

5 Mixed Return and Print


  {
    "language": "beanshell",
    "script": "System.out.println(\"This will not be included\"); return \"Final Result\";"
  }

Expected Output:

"data": "Final Result"

Use case

Create an account using unique incremental ID**

To create an account on delimited file source with increment unique identifier starting from a sequence.

  • Currently there exists no OOTB math operations to perform such kind of data transformation in workflows.

Example:

Endpoint: /api/scripts
Method: POST
Body: {
    "language": "beanshell",
    "script": "String startingSeq = \"{{$.defineVariable.startingSeq}}\";\r\nString totalAccountsInSource = \"{{$.defineVariable.count}}\";\r\nreturn String.valueOf(Integer.parseInt(startingSeq)+Integer.parseInt(totalAccountsInSource));"
}

Response:

{
  "body": {
    "data": "2816",
    "error": null,
    "language": "beanshell",
    "status": 200,
    "timestamp": "2025-09-18T21:38:17.727516684Z"
  },
  "headers": {
    "Content-Type": [
      "application/json"
    ]
  },
  "responseTime": "0.139118 seconds",
  "statusCode": 200
}

The data which has unique incrementor number, can now be used to create an account.

Handle the Arrays

Payload:
{
    "accountRequests": [
        {
            "provisioningTarget": "Active Directory",
            "ticketId": "123xxx2131",
            "attributeRequests": [
                {
                    "operation": "Add",
                    "attributeName": "memberOf",
                    "attributeValue": "CN=sp,DC=sp,DC=example"
                },
                {
                    "operation": "Add",
                    "attributeName": "mail",
                    "attributeValue": "sp@sp.example"
                },
                {
                    "operation": "Add",
                    "attributeName": "sAMAccountName",
                    "attributeValue": "sp.sp.example"
                }
            ]
        }
    ]
}

In order to get the sAMAccountName from Active Directory one might use the JSON Path Expression for the below listed payload:

{{$.accountRequests[?(@.provisioningTarget=='Active Directory')].attributeRequests[?(@.attributeName=='sAMAccountName')].attributeValue}}

The response of the JSONPath results in an Array.

[
  "sp.sp.example"
]

This cannot be stored in “Define Variable”.

But the same when passed through “Escape API” Endpoint would return a string.

Endpoint: /api/escape
Method: POST
Body: {{$.trigger.accountRequests[?(@.provisioningTarget=='Active Directory')].attributeRequests[?(@.attributeName=='sAMAccountName')].attributeValue}}

Response:

{
  "body": {
    "escaped": "[\"test1234\"]",
    "length": 12
  },
  "headers": {
    "Content-Type": [
      "application/json"
    ]
  },
  "responseTime": "0.177894 seconds",
  "statusCode": 200
}

The same can be stored in the DefineVariable Operator and processed further for any usecase.

Escape Data/Code

The endpoint is responsible for escaping any data to be usable in /api/scripts.

1. API Endpoint

POST /api/escape
Content-Type: text/plain

2. Request Format

import java.net.*;
import java.io.*;

URL url = new URL("https://jsonplaceholder.typicode.com/posts/1");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");

BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuffer content = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    content.append(inputLine);
}
in.close();
conn.disconnect();
return content.toString();

3. Response Format


  {
    "escaped": "<escaped Java Code>",
    "length": <int length>
  }


Upload Jars

1. API Endpoint

POST /api/upload-jar
Content-Type: multipart/form-data

2. Request Format

Send a JAR file using multipart/form-data.

POST /api/upload-jar
Content-Type: multipart/form-data
Body: form-data
      file = /select/file/location.jar

cURL Example:

curl -X POST "{{baseUrl}}/api/upload-jar"   -H "Content-Type: multipart/form-data"   -F "file=@com.google.gson_2.10.1.jar"

3. Response Format

Generic structure of API response:

{
  "status": "<uploaded|already_exists|error>",
  "path": "Absolute file path where the JAR was stored",
  "timestamp": "YYYY-MM-DDTHH:MM:SS.sssZ",
  "error": null
}

4. Example Usage in BeanShell Scripts

Once a JAR is uploaded, it can be dynamically loaded in BeanShell scripts using:

addClassPath("/app/uploaded-jars/com.google.gson_2.10.1.jar"); //add the path that was returned by Upload JAR API
//add another JAR Location
//add another JAR Location

import com.google.gson.Gson; //Normal, library import statements for the jar that was uploaded
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

gson = new Gson();

// Create array [ "foo", "bar" ]
JsonArray arr = new JsonArray();
arr.add(new JsonPrimitive("foo"));
arr.add(new JsonPrimitive("bar"));

// Create object { "foo":true, "bar":true, "foo-bar":false }
JsonObject orgs = new JsonObject();
orgs.add("foo", new JsonPrimitive(true));
orgs.add("bar", new JsonPrimitive(true));
orgs.add("foo-bar", new JsonPrimitive(false));

// Wrap into parent object
JsonObject root = new JsonObject();
root.add("array", arr);
root.add("orgs", orgs);

// Return as Map so Spring serializes it as JSON object
return gson.fromJson(root, java.util.Map.class);

Expected Output:

{
    "timestamp": "2025-09-19T13:15:29.938366600Z",
    "data": {
        "array": [
            "foo",
            "bar"
        ],
        "orgs": {
            "foo": true,
            "bar": true,
            "foo-bar": false
        }
    },
    "status": 200,
    "error": null,
    "language": "beanshell"
}

Calling APIs Inside Java Code

1. API Endpoint

POST /api/scripts
Content-Type: application/json

2. Request Format

Note: Must be escaped before sending to /api/scripts.

import java.net.*;
import java.io.*;

URL url = new URL("https://jsonplaceholder.typicode.com/posts/1");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");

BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuffer content = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
    content.append(inputLine);
}
in.close();
conn.disconnect();
return content.toString();

3. Response Format


  {
    "timestamp": "YYYY-MM-DDTHH:MM:SS.sssZ",
    "data": <Returned data>,
    "status": 200,
    "error": null,
    "language": "beanshell"
  }

4. Error Handling


  {
    "language": "beanshell",
    "script": "int x = \"wrong\";"
  }

Expected Output:

"timestamp": "",
"data": null,
"status": 500,
"error": "Encountered \"=\" at line 1..."
3 Likes