| 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. | |
| Legal Agreement | By using this CoLab item, you are agreeing to SailPoint’s Terms of Service for our developer community and open-source CoLab. | |
| Repository Link | GitHub - sailpoint-oss/colab-isc-extended-workflow-helper | |
| 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..."
