9.11 - Services Workbench - Knowledge Base
Services Workbench - Knowledge Base — Knowledge Base articles.
- Service Suite Knowledge Base Overview
- Convert XML Request to JSON
- How to add DPL reference in-service suite script vs Scenario
- How to use Path Parameters in Service Suite?
- How to use Query Parameters in Service Suite?
- How to get DPL data in the service suite?
- How to Validate API Responses in Service Suite
- How is the Request and response POJO management?
- How to validate status code that is returned in response header
- How to read specific row in service suite from CSV?
- Reading data from JSON/XML from Service Suite
- How to add transaction names in Service Suite
- Generating thousands/millions of rows of Synthetic DPL data using Rest APIs
- Run Approved OS Commands from Service Suite
- Validate CAPTCHA Text Capture in Approved Test Environments
- Gmail App Password Use in Service Suite Tests
- Outlook App Password Use in Service Suite Tests
- Validating Emails using Service Suite Scripts
- Microsoft Graph: Send, Read, Reply, and Delete Email in Service Suite
- Sample Service Suite Scripts
- Run Node.js npm Code from Service Suite
- Send Email from Service Suite
- RBAC setup to use Graph Service in Service Suite
Service Suite Knowledge Base Overview
Service Suite Knowledge Base Overview
This overview organizes Service Suite knowledge base content for current AIQ service and API testing. Remove or consolidate articles that are empty, duplicate, or tied only to obsolete examples.
Keep articles that explain
- Reusable Service Suite patterns.
- Request setup, response extraction, assertions, and validation.
- Approved dependency and package usage.
- Integration troubleshooting that applies to current AIQ releases.
Maintenance rules
- Use current product names such as Service Suite and Scenario Editor where applicable.
- Do not include real secrets, tokens, tenant URLs, or customer-specific payloads.
- Prefer concise workflows and validation checklists over long one-off examples.
Last updated: September 8, 2026
Convert XML Request to JSON
This example details how to convert XML responses generated from a Services Workbench script to JSON and then back to XML using Designer .js scripts.
-
Custom
.jsfunctions are written for the conversion of XML to JSON and JSON to XML. in javascript (.js) file. -
In a Services Workbench script, the response data is stored in locker so that it can be retrieved and used in Designer
.jsscript. -
The Designer
.jsscript includes the previous two files, and once the data is retrieved from locker, uses the custom.jsfunction to parse the XML back to JSON and perform the required validations.
XML to JSON Function
Converts XML to JSON and JSON to XML.
Sample Script
/**
* @aiq.webdesigner
* This script requires AIQ Web Designer
*/
function jsonToXml(obj) {
// create xml string
var xml = '';//'<?xml version="1.0" encoding="UTF-8"?>';
//loop through and add each property as a an xml tag as well as sub properties
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) {
continue;
}
if (obj[prop] == undefined)
continue;
xml += "<" + prop + ">";
if (typeof obj[prop] == "object")
xml += jsonToXml(new Object(obj[prop]));
else
xml += convertXMLString(obj[prop], 'xml');
xml += "</" + prop + ">";
}
// searches the string for numbers surrounded by underscores and removes them; used for multiple properties in the json that are set up an an object instead of an array
let reducedXML = xml.replace(/([_]\d)\w+/g, '');
return reducedXML;
}
function convertXMLString(input, outputFormat){
if(typeof input === 'string'){
if(outputFormat === 'xml'){
return input.replace(/(&)/g, '&').replace(/(<)/g, '<').replace(/(>)/g, '>').replace(/(')/g, ''')
} else if(outputFormat === 'string'){
return input.replace(/(<)/g, '<').replace(/(>)/g, '>').replace(/(')/g, "'").replace(/(&)/g, '&')
} else {
// invalid outputFormat
return input;
}
} else {
// not a stringF
return input;
}
}
function parseXmlToJson(xmlString) {
let xmlToParse = xmlString.replace(/[\r\n']/g, match => match === "'" ? "\\'" : ' '); // Replce all the new line and single quote
const json = evalAndReturn(`
const parser = new DOMParser();
const xml = parser.parseFromString('${xmlToParse}', "application/xml");
if (xml.getElementsByTagName("parsererror").length) {
return 'Error';
}
const parseXmlToJson = (xml) => {
let obj = {};
let nodeName = xml.nodeName.includes(":") ? xml.nodeName.split(":")[1] : xml.nodeName;
if (xml.childNodes.length === 1 && xml.firstChild.nodeType === 3) {
return xml.textContent.trim();
}
for (let child of xml.childNodes) {
if (child.nodeType === 3 && !child.textContent.trim()) {
continue; // Ignore whitespace text nodes
}
let childName = child.nodeName.includes(":") ? child.nodeName.split(":")[1] : child.nodeName;
let childJson = parseXmlToJson(child);
if (obj[childName]) {
if (!Array.isArray(obj[childName])) {
obj[childName] = [obj[childName]];
}
obj[childName].push(childJson);
} else {
obj[childName] = childJson;
}
}
return obj;
}
const jsonObj = parseXmlToJson(xml);
return JSON.stringify(jsonObj);
`)
return json;
}
Services Workbench Script
The response data is stored in locker so that it can be retrieved and used in a Designer .js script.
Sample Script
/**
* @aiq.servicesworkbench
* This script requires Services Workbench
*/
var resp = get("https://mocktarget.apigee.net/xml");
var responseData = resp.getDataString();
//log(responseData);
locker.put("XMLData",responseData);
Designer Script
Includes the previous two scripts, and once the data is retrieved from locker, uses the custom .js function to parse the XML back to JSON and perform the required validations.
Sample Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @aiq.webdesigner
* This script requires AIQ Web Designer
*/
include("{ds}/XMLtoJSON&ObjtoXML.js");
serviceSuite('{ds}/XMLJSON.js');
var xmlString = retrieve("XMLData");
log(xmlString);
var jsondata = parseXmlToJson(xmlString);
log(jsondata);
log(JSON.parse(jsondata));
var jsonD = JSON.parse(jsondata);
log(jsonToXml(JSON.parse(jsondata)));
assertEquals("John",jsonD.root.firstName);
-
Lines 5 through 8 : Includes the common JavaScript and Service Workbench scripts. Retrieves the data from the locker and store in a variable.
-
Lines 9 through 11 : Uses the
parseXmlToJsonfunction to convert the response XML to JSON string. Next line it can be parsed and converted to JSON object, from which validations can be performed by extracting the required data. -
Line 13 : JSON to XML conversion is done using the
JsonToXmlfunction.
Resources : Sample Scripts
You can download the sample scripts from these links.
Last updated: March 04, 2025
If you have any feedback on this topic, please contact us at documentation@appvance.ai
How to add DPL reference in-service suite script vs Scenario
Using the below syntax we can run the DPL enabled script from the service suite as well as from the scenario editor without having to change anything from the code.
setVariablesIfNeeded('{ds}/globalProperties.csv','HashDPL',0);
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
How to use Path Parameters in Service Suite?
Path parameters are dynamic values that are included directly in the URL path of an API endpoint.
In Service Suite, you can construct an endpoint URL using JavaScript string concatenation. This allows you to insert variables, such as a client number or customer ID, into the URL.
Example
The following example uses the $clientNumber variable as a path parameter in a POST request:
var $clientNumber = getLoggedClientNumber(); // Or read the value from a CSV file
var $url = $server + "/customer/" + $clientNumber + "/reports";
var $payload = {
"action": "listinvoices",
"from": "10/01/2020"
};
var $response = post($url, $payload);
var $list = JSON.parse($response.getDataString());
How it works
If $server contains:
https://example.com
and $clientNumber contains:
12345
the resulting URL will be:
https://example.com/customer/12345/reports
In this example:
$servercontains the base URL of the API.$clientNumbercontains the dynamic value used as the path parameter./customer/and/reportsare static parts of the endpoint path.post()sends the request to the dynamically constructed URL.JSON.parse()converts the response data into a JavaScript object.
Using a CSV Value
The path parameter can also come from test data, such as a CSV file:
var $clientNumber = $clientNumberFromCSV;
var $url = $server + "/customer/" + $clientNumber + "/reports";
var $response = post($url, $payload);
This approach allows the same Service Suite script to execute the request for different client numbers without changing the endpoint manually.
Summary
Use JavaScript string concatenation when a value in the API endpoint needs to change dynamically during execution:
var $url = $server + "/customer/" + $clientNumber + "/reports";
This is useful for endpoints that follow patterns such as:
/customer/{clientNumber}/reports
/customer/{customerId}/invoices
/orders/{orderId}/details
Last updated: August 17, 2026
If you have any feedback on this topic, please contact us at documentation@appvance.ai.
How to use Query Parameters in Service Suite?
Query parameters are values added to the URL after the ? character. They are commonly used to provide optional parameters or filters to an API request.
In Service Suite, you can construct a URL dynamically using JavaScript string concatenation and variables.
Example
The following example uses $clientNumber as a query parameter in a GET request. It also adds a timestamp as a cache-busting parameter:
var $clientNumber = getLoggedClientNumber(); // Or read the value from a CSV file
var $url = $server + "/common/getClientData/?clientNumber=" + $clientNumber + "&_=" + Date.now();
var $response = get($url);
var $json = JSON.parse($response.getDataString());
var $clientNumber = $json.clientNumber;
How it works
If $server contains:
https://example.com
and $clientNumber contains:
12345
the resulting URL will look similar to:
https://example.com/common/getClientData/?clientNumber=12345&_=1750000000000
The URL contains two query parameters:
clientNumber=12345— passes the client number to the API._=followed by the current epoch time — adds a unique value to help prevent the request from being served from a cached response.
Date.now() returns the current time in milliseconds since the Unix epoch.
Multiple Query Parameters
When an endpoint requires multiple query parameters, use ? before the first parameter and & before each additional parameter.
For example:
var $url = $server + "/common/getClientData/?clientNumber=" + $clientNumber + "&status=active";
This produces a URL such as:
https://example.com/common/getClientData/?clientNumber=12345&status=active
Query Parameters vs. Path Parameters
A path parameter is part of the URL path:
/customer/12345/reports
A query parameter is included after ?:
/customer/reports?clientNumber=12345
The API determines which format should be used for each endpoint.
Summary
Use JavaScript string concatenation to dynamically construct URLs containing query parameters:
var $url = $server + "/common/getClientData/?clientNumber=" + $clientNumber;
When adding more than one query parameter, separate them with &:
var $url = $server + "/common/getClientData/?clientNumber=" + $clientNumber + "&status=active";
Last updated: August 1, 2026
If you have any feedback on this topic, please contact us at documentation@appvance.ai.
How to get DPL data in the service suite?
The below code iterates on the columns and fetches the DPL data we want to use in the script.
setVariablesIfNeeded('test.csv','HashDPL',0,'en_US');
var Length = 5;
for (j=1; j<=Length; j++) {
var val = eval("$Test"+j);
log(val);
}
Example Code
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
How to Validate API Responses in Service Suite
When executing API requests in Service Suite, you can validate the response to verify that the request was successful.
You can validate information from the response body, such as a success flag or message, as well as HTTP response properties such as the status code.
Validate a Response Using JavaScript
The following example parses the response body and throws an error if the API indicates that the operation was not successful:
var $response = post($url, $payload);
var $jsonList = JSON.parse($response.getDataString());
if ($jsonList.isSuccess !== "true") {
throw "Call was not successful. Message: " + $jsonList.message;
}
In this example:
post()sends the request to the endpoint.$responsecontains the HTTP response.getDataString()retrieves the response body as a string.JSON.parse()converts the JSON response into a JavaScript object.- The
isSuccessproperty is checked to determine whether the API call was successful. - If the call was unsuccessful,
throwstops the execution and displays the message returned by the API.
Validate a Response Using assertEquals
Instead of using an if statement, you can use an assertion to validate a value in the response:
var $response = post($url, $payload);
var $jsonList = JSON.parse($response.getDataString());
assertEquals($jsonList.isSuccess, "true");
The assertion fails if isSuccess does not equal "true".
Validate the HTTP Status Code
You can also validate the HTTP status code returned by the server:
assertEquals($response.status, 200);
This verifies that the response status is 200 (OK).
For example, you can combine a status-code validation with a response-body validation:
var $response = post($url, $payload);
assertEquals($response.status, 200);
var $jsonList = JSON.parse($response.getDataString());
assertEquals($jsonList.isSuccess, "true");
This allows you to verify both:
- The HTTP request returned the expected status code.
- The API response body contains the expected result.
Summary
Service Suite provides multiple ways to validate API responses:
- Use
throwwhen you want to stop execution with a custom error message. - Use
assertEquals()to validate values returned in the response body. - Use
$response.statusto validate the HTTP status code returned by the server.
Using these validations helps ensure that API requests not only execute successfully but also return the expected results.
Last updated: September 1, 2026
If you have any feedback on this topic, please contact us at documentation@appvance.ai.
How is the Request and response POJO management?
Internally when you call a createPost() command (or createGet, createOptions, etc), it returns an HttpRequest object. In the following snippet, the $call variable is an instance of an HttpRequest object that when processed will trigger a POST call:
var $call = createPost($url, $payload);
var $call.addHeader("Authorization","Basic "+$TOKEN);
var $response = process($call);
log("Status: "+$response.status);
And the variable $response is an instance of an HTTPResponse object.
Some of the most commonly used properties and methods can be found in the following examples.
HttpRequest
var $data = { "some":"data"};
var $call = createPost("https://www.apple.com",$data);
$call.addHeader("someheader","somevalue");
log($call.getHeader("content-type"));
log($call.postMimeType());
log($response.recreateHeader()); // returns request headers raw text
log($call.url());
log($call.contentLength());
log($call.getDataString());
HttpResponse
var $response =get("http://worldclockapi.com/api/json/est/now");
log($response.status); // 200
log($response.statusText); // OK
log($response.getDataString());
log($response.getHeader("Server"));
log($response.recreateHeader()); // returns response headers raw text
log($response.getResponseType()); // JSON
log($response.contentLength());
log($response.isBasic());
log($response.isNTLM());
log($response.isNegotiate());
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
How to validate status code that is returned in response header
If we want to validate the status code that is returned in the response header we can use the below approach.
var $response = post($url, $payload );
assertEquals($response.status, 200);
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
How to read specific row in service suite from CSV?
By providing the number after the csv, Service suite should be able to read from the specific row.
HashDPL(“{mds}/data/data.csv”,1);
Last updated: February 17, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
Reading data from JSON/XML from Service Suite
You can read JSON data from a file and convert it into a JavaScript object using the following example:
// Read the JSON file and convert its contents to a string.
var $aJsonString = new java.lang.String(
readBytes("{mds}/somejsonfile.json")
);
// Parse the JSON string into a JavaScript object.
var jsonObj = JSON.parse($aJsonString);
The example performs two steps:
- Read the file:
readBytes()reads the contents of the JSON file. The contents are converted to a JavaString. - Parse the JSON:
JSON.parse()converts the JSON string into a JavaScript object, allowing you to access individual values from the JSON data.
For example, if the JSON file contains:
{
"username": "mauricio",
"environment": "QA"
}
You can access its values through the resulting object:
log(jsonObj.username);
log(jsonObj.environment);
This produces:
mauricio
QA
Last updated: August 13, 2026
If you have any feedback on this topic, please contact us at documentation@appvance.ai.
How to add transaction names in Service Suite
You can use the startStep() API to assign a descriptive name to a transaction or step in a Service Suite script. The transaction name is then displayed in the execution reports, making it easier to identify and analyze individual transactions.
Example
The following example assigns the transaction name "Open web page API simulation" before executing the request:
addGlobalHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36");
addGlobalHeader("cache-control", "no-cache");
startStep("Open web page API simulation");
var resp = open("https://www.apple.com");
How it works
startStep("Open web page API simulation")starts a named transaction/step.- The string passed to
startStep()is used as the transaction name in the reports. - The
open()call executes the request associated with that transaction.
You can replace the transaction name with any meaningful description that helps identify the operation being measured.
For example:
startStep("Login API");
var resp = open("https://example.com/login");
This allows the transaction to be easily identified as "Login API" in the corresponding reports.
Last updated: August 17, 2026
If you have any feedback on this topic, please contact us at documentation@appvance.ai.
Generating thousands/millions of rows of Synthetic DPL data using Rest APIs
Below is the Service Suite script that can be used to generate synthetic data using Rest APIs, this would be useful when generating millions of data which might be difficult using the UI as UI will have its own limitations.
Start AIQ
Go to Service Suite
API editor
Open the below script
modify the quantity variable to 1000000 (or as desired)
Save and run
a file with 1000000 will be created.
Note: There are 2 parameters that might be required to change.
-
AIQ URL (Line 1) -
var $baseURL = "http://localhost:8080"; -
Creds (Line 12 and 13) - Username and password.
var $baseURL = "http://localhost:8080";
function getTimestamp() {
return (new Date()).getTime();
}
//
// LOGIN
//
function login($baseURL) {
var $loginForm = formData();
$loginForm.put("username", "appvance");
$loginForm.put("password", "appvance");
var $call = createPost($baseURL + "/AppvanceServer/rest/admin/logginForServices", $loginForm);
var $resp = process($call).getDataString();
if ($resp.indexOf("true") != -1) {
//
// GET JSESSIONID COOKIE
//
var $jsession = getCookieValue($baseURL, "JSESSIONID");
addGlobalHeader("cookie", "JSESSION = " + $jsession);
log("Login successful, JSESSION:" + $jsession)
return true;
} else {
return false;
}
}
var $resp = login($baseURL);
var quantity = 100000;
var fileName = "REPOSITORY/HOME/DPL.csv";
if ($resp === true) {
log("Is Logged");
var data = [["qty", "test"],["syn.random.number(10)","syn.random.number(100)"]];
var call = createPost($baseURL + "/AppvanceServer/rest/syntheticDPL/evaluate?lines="+quantity+"&locale=en_US", data);
var stringJson = process(call).getDataString();
var payload = JSON.parse(stringJson.replace(/\n|\r/g, "").trim());
var callSave = createPost($baseURL + '/AppvanceServer/rest/file/updateCSVFile?commitMessage=&path=' + fileName, payload);
var stringJsonSave = process(callSave).getDataString();
var response = JSON.parse(stringJsonSave);
log(fileName + " was saved: " + response.status);
}
//
// LOGOUT
//
var $resp = get($baseURL + "/AppvanceServer/rest/admin/logout");
log("logout: " + $resp.getDataString());
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
Run Approved OS Commands from Service Suite
Last reviewed: September 30, 2026
Use this article only when a Service Suite scenario must run an approved operating-system command through AIQ in a controlled execution environment.
What changed in this article
- Reframed command-prompt execution as a controlled Service Suite capability instead of a general command-running shortcut.
- Added security checks for command allowlisting, execution host, working directory, quoting, secrets handling, stdout/stderr review, timeout behavior, and least-privilege permissions.
- Removed unsafe sample-command emphasis and replaced it with validation guidance that avoids encouraging arbitrary local command execution.
Validation workflow
- Confirm the command is approved for the target AIQ environment and cannot expose credentials or modify unintended files.
- Validate the working directory, arguments, permissions, expected output, timeout, and failure behavior in a non-production run.
- Review stdout, stderr, exit status, and Service Suite logs before using the command in a scheduled or shared workflow.
Review note: This page is now intentionally cautious because OS command execution can affect the execution host.
Last updated: September 30, 2026
If you have feedback on this topic, contact documentation@appvance.ai.
Validate CAPTCHA Text Capture in Approved Test Environments
Validate CAPTCHA Text Capture in Approved Test Environments
Use this article only for approved test environments where CAPTCHA images are provided as controlled test fixtures. Do not use AIQ automation to bypass CAPTCHA or defeat anti-bot protections on production systems.
Safe testing approach
- Use a sandbox CAPTCHA, static fixture, or customer-approved test mode.
- Validate OCR or text-extraction behavior only when the CAPTCHA provider and application owner permit it.
- Keep expected values in test data, not in production user flows.
- Record whether the test is validating OCR quality, application handling, or a downstream business rule.
- Use Service Suite or Scenario Editor assertions to verify the expected result without weakening real security controls.
Validation workflow
- Confirm written approval or documented test-mode support for the CAPTCHA scenario.
- Run the test with a known fixture and expected extracted value.
- Validate success and failure behavior separately.
- Review logs to ensure no real user credentials, session data, or sensitive images are captured.
Security note: CAPTCHA is a security control. This content must not be used as a guide for bypassing CAPTCHA on live systems.
Last updated: October 8, 2026
Gmail App Password Use in Service Suite Tests
Last reviewed: September 24, 2026
This legacy article applies only when a Google account is explicitly allowed to use app passwords for automated Service Suite mail validation.
What changed in this article
- Prefer Gmail API/OAuth or an approved test-mail service for current implementations.
- Use a dedicated test account; do not automate a personal mailbox or production user account.
- Treat the app password as a secret and remove it from logs, screenshots, exported results, and sample code.
Validation workflow
- Confirm that the Google Workspace policy permits app passwords for the selected test account.
- Create the password outside AIQ and store it only in the approved secret manager.
- Run the Service Suite script with a small mailbox test and verify no secret value appears in output.
Review note: Marked the Gmail app-password flow as legacy/security-sensitive and added OAuth/test-mail alternatives.
Last updated: September 24, 2026
If you have feedback on this topic, contact documentation@appvance.ai.
Outlook App Password Use in Service Suite Tests
Last reviewed: September 24, 2026
This legacy article applies only when an Outlook or Microsoft 365 account still allows app passwords for automated Service Suite login flows.
What changed in this article
- Prefer Microsoft Graph, OAuth, or an approved test-mail API when available; app passwords should not be the default recommendation.
- Never store the app password in the article, screenshots, source control, or reusable sample scripts.
- Use a dedicated test mailbox and rotate the app password according to the customer security policy.
Validation workflow
- Confirm with the Microsoft 365 administrator that app passwords are allowed for the tenant and test account.
- Generate the app password outside AIQ and store it only in the approved secret location.
- Run the Service Suite script against a non-production mailbox and remove any captured secret from logs.
Review note: Marked the Outlook app-password approach as legacy/security-sensitive and replaced the long title with a concise scope-focused title.
Last updated: September 24, 2026
If you have feedback on this topic, contact documentation@appvance.ai.
Validating Emails using Service Suite Scripts
-
Create a Script in Service suite as shown below:
-
Please refer the below document if the outlook account as 2FA authentication:
Note: Below is the spec on AIQ mail client
Mail Client
>createMailClient(String host, String userMail, String password, String protocol)
returns a MailService object.
>MailService object has the following available methods:
-
login()
-
setFromFolder(String:folder)
-
logout()
-
getLastUnreadMessages(int:quantity)
-
getAllRecipients();
-
getFrom();
-
message.getSentDate();
-
message.getFolder()
-
MailMessage byte[] getAttachedFile(String fileName)
· List<String> getAttachments() //You can get a list with all the names of the attachments from a MailMessage and you can get the file (byte[]) using the file name.
Sample code snippet
var mailClient = createMailClient("imap.gmail.com", "myEmail@gmail.com", "App Password", "IMAP");
try{
mailClient.login();
}
catch (e){
mailClient.login();
}
log("Login Outlook Account");
mailClient.setFromFolder("Inbox");
log("Navigate to the Inbox folder");
//mailClient.setFromFolder("Junk Email");
var lastUnreadMails = mailClient.getLastUnreadMessages(5);
var url;
var attachments;
//log(lastUnreadMails.size());
for(var i = 0; i < lastUnreadMails.size(); i++){
if(lastUnreadMails.get(i). getFrom(). contains("Appvance")){
log("Fetch the url from this subject" + lastUnreadMails.get(i).getSubject());
log("From "+lastUnreadMails.get(i).getFrom());
log("Date "+lastUnreadMails.get(i).getSentDate());
log("Folder "+lastUnreadMails.get(i).getFolder());
log("getRecipients "+lastUnreadMails.get(i).getRecipients());
log("Fetch the url from this body" + lastUnreadMails.get(i).getBody());
attachments = lastUnreadMails.get(i).getAttachments();
for(var j = 0; j < attachments.size(); j++){
log("Attachment name: "+attachments.get(j));
var bytes = lastUnreadMails.get(i).getAttachedFile(attachments.get(j));
locker.put("bytes",bytes);
writeBytes("newBytes.pdf", bytes);
}
//code = urlify(lastUnreadMails.get(i).getBody());
}
// log("Fetch the url from this body" + lastUnreadMails.get(i).getSubject());
}
mailClient.logout();
Note: Replace the valid Email account to test the above methods.
Outlook Sample
var mailClient = createMailClient("imap.outlook.com", "aiqtest@outlook.com", "password", "IMAP");
mailClient.login();
mailClient.setFromFolder("Inbox");
var lastUnreadMails = mailClient.getLastUnreadMessages(1);
var url = "";
var from=lastUnreadMails.get(0).getFrom().contains("Appvance");
log(from);
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
Microsoft Graph: Send, Read, Reply, and Delete Email in Service Suite
Microsoft Graph: Send, Read, Reply, and Delete Email in Service Suite
Use this article when a Service Suite workflow needs to validate Microsoft Graph email operations in an approved test mailbox. Do not run destructive email actions against real customer or employee mailboxes unless the customer has explicitly approved the test.
Configuration checklist
- Use a dedicated test mailbox and a Microsoft Entra ID app registration approved for the workflow.
- Grant only the Graph permissions required for send, read, reply, or delete operations.
- Store client secrets, certificates, or tokens in secured variables or an approved secret store.
- Use test subjects, correlation IDs, or message metadata so the workflow finds the correct email.
- Separate read-only validation from delete or cleanup steps.
Validation workflow
- Confirm token acquisition and Graph endpoint access with the test mailbox.
- Send or locate a controlled test message.
- Validate the response status, message ID, subject, recipient, and body fields needed by the test.
- Reply or delete only the controlled message created for the test run.
- Review logs to confirm secrets and message content are not exposed unnecessarily.
Last updated: October 8, 2026
Sample Service Suite Scripts
Here are some Service Suite samples scripts that can be used as a reference.
Basic Authentication
/**
* btoa
* @param st
* @returns base64 encoded string
*/
function btoa(st) {
return Java.type("java.util.Base64").getEncoder().encode(st);
}
var $username = "admin";
var $password = "Password@1";
var $TOKEN = "Basic "+btoa($username+":"+$password);
addGlobalHeader("authorization", $TOKEN);
var url = "https://someServer.com/api/somecall";
var data = {"short_description":"some data",
"description":"some description"};
var call = createPost(url, data);
call.addHeader("Content-Type", "application/json");
var resp = process(call);
Demosite
var formPage = get("https://demosite.appvance.com/products/ruby-on-rails-tote");
log(getCookies("https://demosite.appvance.com/"));
var token = extract("html",formPage,'//form[@action="/orders/populate"]/input[@name="authenticity_token"]/@value');
log("Extracted token: "+token);
var form = formData();
form.put("authenticity_token",token);
form.put("utf-8",decodeURIComponent("%E2%9C%93"));
form.put("variant_id","1");
form.put("quantity","1");
form.put("button","");
var postData = createPost("https://demosite.appvance.com/orders/populate",form);
var purchaseResults = process(postData);
log("response:\n"+purchaseResults.getDataString());
assertContains(get("https://demosite.appvance.com/cart?variant_id=1").getDataString(),"Ruby on Rails Tote");
assertContains(get("https://demosite.appvance.com/cart_link").getDataString(),"$15.99");
Login to AIQ
$baseURL = "http://localhost:8080";
var $loginForm = formData();
$loginForm.put("username","appvance");
$loginForm.put("password","appvance");
var $call = createPost($baseURL+"/AppvanceServer/rest/admin/loggin", $loginForm);
var $resp = process($call);
var $json = JSON.parse($resp.getDataString());
if ($json.logged) {
var $jsession = getCookieValue($baseURL,"JSESSIONID");
addGlobalHeader("cookie","JSESSIONID="+$jsession); // adding global cookie for JSESSIONID
log("Logged In, JSESSIONID: "+$jsesssion);
} else {
log("Unsuccessful login");
}
ServiceSuite and XML
var payload = "<xml><username>John</username><id>123-1341-123</id></xml>";
var call = createPost("https://example.com/getAddress", payload, "text/xml");
var result = process(call);
var address1 = extract("XML", result, "//Address1");
var city = extract("XML", result, "//City");
var state = extract("XML", result, "//ST");
Upload a file to AIQ repository
const $boundary = "AIQ-DS-BOUNDARY";
function login($baseURL, $username, $password) {
var $loginForm = formData();
$loginForm.put("username",$username);
$loginForm.put("password",$password);
var $call = createPost($baseURL + "/AppvanceServer/rest/admin/logginForServices", $loginForm);
var $resp = process($call).getDataString();
if ($resp.indexOf("true") != -1) {
var $jsession = getCookieValue($baseURL,"JSESSIONID");
addGlobalHeader("cookie","JSESSION=" + $jsession);
log("Login successful")
return true;
} else {
log("Login was NOT successful")
return false;
}
}
function createMultipartPayload($filename) {
var header = '--' + $boundary + '\n' +
'Content-Disposition: form-data; name="file"; filename="' + $filename + '"\r\n' +
'Content-Type: application/octet-stream\r\n\r\n';
var headerBytes = new Array(header.length);
for (var i = 0; i < header.length; ++i) {
headerBytes[i] = header.charCodeAt(i);
}
var docBytes = readBytes($filename);
var footer = '\r\n--' + $boundary + "--\r\n";
var footerBytes = new Array(footer.length);
for (var i = 0; i < footer.length; ++i) {
footerBytes[i] = footer.charCodeAt(i);
}
return headerBytes.concat(docBytes).concat(footerBytes);
}
var $filename = "test2.js"; // CHANGE FOR YOUR FILE TO UPLOAD
var $repositoryName = "test"; // CHANGE FOR YOUR REPO NAME, THIS CONF MUST BE IN THE USER ACCOUNT USED TO LOGIN
var $username = "appvance";
var $password = "appvance";
login("http://my-aiq-controller.appvance.com:8080", $username, $password); // CHANGE FOR YOUR AIQ SERVER DNS
var data = createMultipartPayload($filename);
log(data.length, $username, $password);
var call= createPost("http://localhost:8080/AppvanceServer/rest/file/uploadFile?&commitMessage=Appvance+skip+commit&path=REPOSITORY/"+$repositoryName+"/"+$filename, data, "multipart/form-data", " boundary=" + $boundary);
var resp = process(call);
log(resp.getDataString())
Upload the File to an application
/**
* Creates a multipart payload from file contentes
**/
function createMultipartPayload($filename) {
var header = '--' + $boundary + '\n' +
'Content-Disposition: form-data; name="file"; filename="' + $filename + '"\r\n' +
'Content-Type: application/octet-stream\r\n\r\n';
var headerBytes = new Array(header.length);
for (var i = 0; i < header.length; ++i) {
headerBytes[i] = header.charCodeAt(i);
}
var docBytes = readBytes($filename);
var footer = '\r\n--' + $boundary + "--\r\n";
var footerBytes = new Array(footer.length);
for (var i = 0; i < footer.length; ++i) {
footerBytes[i] = footer.charCodeAt(i);
}
return headerBytes.concat(docBytes).concat(footerBytes);
}
/*
* Uploads a file to $SERVER
*/
function uploadFile($filename) {
var data = createMultipartPayload($filename);
var url = $SERVER+ "/upload/"+$filename+"?filename="+$filename;
log("url: "+url);
var call= createPut(url, data, "multipart/form-data", " boundary=" + $boundary);
return process(call).getDataString();
}
const $boundary = "AIQ-DS-BOUNDARY";
const $SERVER = "http://xxxx.eu-west-2.elb.amazonaws.com";
var $filename = "test2.js"; // CHANGE FOR YOUR FILE TO UPLOAD
var result = uploadFile($filename);
log(result);
Send an email with a custom timestamp in the Subject
var date = new Date();
var formatDate = date.getDate() + "-" + date.getMonth() + "-" + date.getYear() + " " + date.toLocaleTimeString();
var graphService = createOAuthMailClient("email", "clientID", "secretKey", "tenantID");
var msg = graphService.createMailMessage("Message generated " + formatDate, "Test body email.");
msg.addTo(["to@test.com"]);
var sent = graphService.sendMessage(msg);
if (!sent) {
throw new Error('An error occurred while sending the email.');
} else {
log("Email sent.");
}
Extract value from receive email’s subject
var graphService = createOAuthMailClient("email", "clientID", "secretKey", "tenantID");
var unreadList = graphService.getUnreadMessages();
var unreadCount = unreadList.size();
if (unreadCount > 0) {
var i = 0; //first unread email
log("Id: " + unreadList.get(i).id);
log("From: " + unreadList.get(i).from.emailAddress.address);
log("Subject: " + unreadList.get(i).subject);
var subject = unreadList.get(i).subject;
var startIndex = subject.lastIndexOf("Ref. ") + 5;
var refCode = subject.substr(startIndex, 8);
log("Ref code: " + refCode);
Compare email’s body
var expectedBody = "This is an automated acknowledgement to let you know that your correspondence to the Appvance Information Rights Team has been received.Any request for personal data will be handled as a data subject access request.";
var body = unreadList.get(i).body.content.replace(/(\r\n|\n|\r)/gm,""); //remove line break and spaces.
body = body.substr(0,body.indexOf("uk")+2); //trim to remove links at the end
log("Email have the expected content: " + (body === expectedBody));
} else {
log("There's no unread messages.");
}
Utility Functions
/**
* btoa
* @param st
* @returns base64 encoded string
*/
function btoa(st) {
return Java.type(java.util.Base64).getEncoder().encode(st);
}
/**
* atob
* @param st
* @returns decoded base64 string
*/
function atob(st) {
return Java.type(java.util.Base64).getDecoder().decode(st);
}
/**
*
* @param length number
* @returns random number of length number of digits
*/
function randomNumber(length) {
var result = '';
for (var i = 0; i < length; i++) {
result += Math.floor(Math.random() * 10);
}
return result;
}
/**
* Extracts catching an exception and returning NOT_FOUND string if target is not found.
* @param {string} format ("HTML","HTTP","PDF","CSV","Excel","TEXT","STRING")
* @param {HttpResponse} response
* @param {string} target
* @returns
*/
function extractOrNotFound(format, response, target) {
const NOT_FOUND = "Not found.";
var value;
try {
value = extract(format, response, target);
if (typeof value == "undefined") {
return NOT_FOUND;
}
return value;
} catch (e) {
return NOT_FOUND;
}
}
/**
* SSN
* @returns random string matching social security number format
*/
function SSN() {
var result = '';
result = randomNumber(3) + "-" + randomNumber(2) + "-" + randomNumber(4);
return result;
}
// *************************************************************************
// parseCurrencyStringToNumber
//
// Parse a currency string to a number, $1,245.50 -> 1245.5
// ************************************************************************
function parseCurrencyStringToNumber(currency) {
if (typeof currency == "undefined" || currency == "") {
log("Warning in parseCurrencyStringToNumber, expecting a number but got undefined or empty.");
return -1;
}
if (currency == "0") {
return 0;
}
if (currency.indexOf("(") !== -1) {
currency = "-" + currency.replace(/[(|)]+/g, "")
}
return Number(String(currency).replace(/[^0-9.-]+/g, ""));
}
// *************************************************************************
// parseNumberToCurrency
//
// Parse a number to a string currency with commas, 1245.5 -> $1,245.50
// ************************************************************************
function parseNumberToCurrency(aNumber) {
if (aNumber == 0) {
return ("$0.00");
};
var tempST = String(aNumber).replace(/[^0-9.-]+/g, "");
try {
var tempNumber = Number(parseFloat(tempST).toFixed(2));
var tempLocale = tempNumber.toLocaleString(undefined, { minimumFractionDigits: 2 });
return "$" + tempLocale;
} catch (e) {
log("Error converting to currency [" + aNumber + "], isNAN: " + isNaN(aNumber) + " - " + e.message);
return "NA";
}
}
/**
* dateToString
* @param day
* @returns
*/
function dateToString(day) {
var dd = String(day.getDate()).padStart(2, '0');
var mm = String(day.getMonth() + 1).padStart(2, '0'); //January is 0!
var yyyy = day.getFullYear();
return mm + '/' + dd + '/' + yyyy;;
}
function getTodayDate() {
var now = java.time.LocalDateTime.now();
var formatter = java.time.format.DateTimeFormatter.ofPattern("MM/dd/yyyy");
return now.withHour(0).withMinute(0).withSecond(0).format(formatter);
}
function getYesterdayDate() {
var now = java.time.LocalDateTime.now();
var formatter = java.time.format.DateTimeFormatter.ofPattern("MM/dd/yyyy");
return now.minus(1, java.time.temporal.ChronoUnit.DAYS).withHour(0).withMinute(0)
.withSecond(0).format(formatter);
}
function getTomorrowDate() {
var now = java.time.LocalDateTime.now();
var formatter = java.time.format.DateTimeFormatter.ofPattern("MM/dd/yyyy");
return now.plus(1, java.time.temporal.ChronoUnit.DAYS).withHour(0).withMinute(0)
.withSecond(0).format(formatter);
}
/**
* getDateISOString
* @returns
*/
function getDateISOString() {
var st = new Date().toISOString();
return st.replace(/\-/g, "_").replace(/\:/g, "_").replace(/\./g, "_");
}
/**
* dateOneMonthAfter
* @param $date
* @returns
*/
function dateOneMonthAfter($date) {
var day = new Date(Date.parse($date));
day.setMonth(day.getMonth() + 1);
day.setTime(day.getTime() + TIME_OFFSET);
return dateToString(day);
}
/**
* dateOneYearAfter
* @param $date
* @returns
*/
function dateOneYearAfter($date) {
var day = new Date(Date.parse($date));
day.setFullYear(day.getFullYear() + 1);
day.setTime(day.getTime() + TIME_OFFSET);
return dateToString(day);
}
/**
* randomLastName
* @returns
*/
function randomLastName() {
var sylabs = 2 + Math.floor(Math.random() * 2)
var result = '';
var characters = 'bccdfghjkllmnnprrssttvwyz';
var vocals = 'aeioeua';
var charactersLength = characters.length;
result += "SBCLKJMCH".charAt(Math.floor(Math.random() * 9));
result += vocals.charAt(Math.floor(Math.random() * 7));
sylabs--;
for (var i = 0; i < sylabs; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
result += vocals.charAt(Math.floor(Math.random() * 7));
}
result += characters.charAt(Math.floor(Math.random() * charactersLength));
return result;
}
Last updated: June 02, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai
Run Node.js npm Code from Service Suite
Run Node.js npm Code from Service Suite
Use this guidance when a Service Suite scenario needs to call Node.js code or an npm package. Keep package execution controlled, repeatable, and limited to approved dependencies.
Before execution
- Confirm the package is approved for the AIQ environment.
- Pin dependency versions through the approved package-management process.
- Keep credentials, tokens, and environment-specific values out of source files and articles.
- Document expected input, output, exit code, stdout, and stderr behavior.
Validation workflow
- Run the Node.js code with a small local or non-production sample.
- Confirm Service Suite can locate the script and dependencies.
- Review output and failures before using the package in a suite or scheduled run.
- Record dependency changes with the scenario maintenance notes.
Last updated: September 8, 2026
Send Email from Service Suite
Use this article when a Service Suite workflow needs to send email through an approved mail provider or internal SMTP path.
What changed in this article
- Updated the email article around approved Service Suite email workflows and mail-provider controls.
- Added checks for SMTP/API endpoint, authentication, sender ownership, recipient controls, TLS, rate limits, retry behavior, and audit logging.
- Added guidance to avoid hard-coded passwords, personal mailboxes, and sensitive test data in email content.
Validation workflow
- Confirm the approved mail provider, sender identity, recipient list, and allowed test data before sending email.
- Validate authentication, TLS, request payload, response status, and retry/error behavior in a controlled run.
- Review logs and received messages without exposing passwords, tokens, or sensitive customer data.
Review note: This page now treats email sending as an integration with data-transmission risk controls.
Last updated: October 5, 2026
If you have feedback on this topic, contact documentation@appvance.ai.
RBAC setup to use Graph Service in Service Suite
RBAC (Role-Based Access Control) for applications will eventually replace application access policies (see below). Today, the public preview shows some ragged edges in terms of the integration between Exchange Online and Azure AD that Microsoft says they’ll fix before general availability. Even with some bumps to navigate, the new functionality is a welcome addition that extends tenant control over their data, which is always a good thing.
In order to use this protocol you need to do the following configurations in your azure account so the roles and permissions are set correctly.
Once you have done the below steps you should be able to use the service suite Graph Service oauth2 client to send/receive emails using RBAC with the normal client commands.
-
Open Windows PowerShell as an Administrator
-
Execute:
connect-mggraph -scopes "Application.Read.All". If the exception "The term connect-mggraph' is not recognized as the name of a cmdlet" appears, execute the commandinstall-module microsoft.graph -
After Microsoft Graph is installed, execute again the command
connect-mggraph -scopes "Application.Read.All" -
Sign in
-
After signing in, should see this:
-
Execute:
$SP = Get-MgServicePrincipal -All -
Execute:
$ServicePrincipalId = $SP | Where-Object {$_.displayName -eq "{Application name}"} | Select-Object -ExpandProperty Id -
Execute:
$AppId = $SP | Where-Object {$_.displayName -eq "{Application name}"} | Select-Object -ExpandProperty AppId -
Execute:
Write-Host ("AppId is {0} and Service Principal Id is {1}" -f $AppId, $ServicePrincipalId)
Output: -
Execute:
Install-Module -Name ExchangeOnlineManagement -
Execute:
Import-module ExchangeOnlineManagement -
Execute:
Connect-ExchangeOnline -UserPrincipalName {user email}
Output: -
Execute:
New-ServicePrincipal -AppId $AppId -ServiceId $ServicePrincipalId -DisplayName '{Application name}' -
Execute:
New-ManagementScope -Name "Management emails" -RecipientRestrictionFilter "CustomAttribute1 -eq '{RolName}'" -
Execute:
Set-Mailbox -Identity {username} -CustomAttribute1 "{RolName}" -
Execute:
Get-EXOMailbox -Filter {CustomAttribute1 -eq "{RolName}"} | Format-Table DisplayName -
Execute:
New-ManagementRoleAssignment -App $AppId -Role "Application Mail.Mail.ReadWrite" -CustomResourceScope "Management emails" -
Execute:
New-ManagementRoleAssignment -App $AppId -Role "Application Mail.Send" -CustomResourceScope "Management emails"
Assing a role to a user
Execute: Set-Mailbox -Identity {username} -CustomAttribute1 "{RolName}"
List available permissions
Execute: Get-ManagementRole | Where-Object {$_.Name -like "Application *"}
See permissions assigned to a user
Execute: Test-ServicePrincipalAuthorization -Identity $AppId -Resource {email}
Last updated: May 26, 2023
If you have any feedback on this topic, please contact us at documentation@appvance.ai