ISC Extended Workflow Helper


:spiral_notepad: Description isc-extended-workflow-helper extends SailPoint IdentityNow 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 ISC’s 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
:hospital: Supported by Community Developed

Overview

The project focuses on extending the capabilities of sailpoint ISC workflows by integrating with ISC workflows as 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