Custom Code and Macro
Reference
This document provides a complete reference for the
ExecutorCustomCode macro system, including workflow step macros, event
listeners, and field validators. It covers all available capabilities,
how to use them, and their current implementation status.
Related Administration Pages:
1. Macro Requirements
A. One Macro per Workflow
Step
Each workflow step can have a Custom Code block that
executes when the step is accepted or denied.
How to configure:
- Navigate to Settings > Workflows
- Select a workflow from the list
- Click the Code cell for the desired workflow
step
- Write JavaScript code in the integrated code editor
- Click Compile to verify syntax, then
Save
How it works:
- The macro runs when the workflow step is triggered (accepted or
denied)
- The
userAction.Result object tells you whether the step
was accepted (ActionResult.Yes) or denied
(ActionResult.No)
- Each step can have exactly one code block
Example:
if (userAction.Result === ActionResult.Yes) {
await setStatus("Approved");
await sendMailTo(
["manager@example.com"],
"Step approved for Ticket #" + (await getTicketValue("PunchItem")),
"Workflow Progress"
);
} else {
await stepBackwardJumpTo(1, "Returned to initial review");
}
Note: In addition to workflow step macros, you can
use Event Listeners for
event-driven automation that is not tied to a specific step action.
B. Workflow-Level Event
Macros
Event listeners enable macros that trigger on specific events across
the application. Unlike step macros, event listeners react to conditions
such as field changes, email receipts, and scheduled intervals.
How to configure:
- Navigate to Settings > Listeners
- Click the Add (+) button
- Select a Type from the dropdown
- Configure the required fields for that type
- Write Custom Code and save
Available Event Types
| Event Type |
Trigger |
Required Fields |
onWorkflowStepAccepted |
A workflow step is accepted |
Checked Field (step number), Checked Value, Workflow ID |
onValueChanged |
A ticket field value matches a condition |
Checked Field (field name), Checked Value |
onTicketCreated |
A new ticket is created |
Default (typically true) |
onInteractionEmailSent |
An interaction email is sent |
Subject Regex, Content Regex |
onTicketOverdue |
Scheduled check for overdue tickets |
Interval (cron), Overdue (ms) |
onEmailReceived |
An inbound email is received |
Subject Regex, Content Regex, Interval (cron) |
ValueChangeValidating
Field validators provide a pre-save validation event. When a ticket
field value changes, all active validators for that field run
before the value is saved.
How to configure:
- Navigate to Settings > Workflows >
Validators
- Select a workflow from the dropdown
- Click Add to create a new validator
- Select one or more fields from the Fields
TagBox
- Enter an Error Message (shown to the user on
failure)
- Write validation code that returns
true (allow) or
false (block)
See Field
Validators for full details.
ValueChanged (onValueChanged)
Triggers after a ticket field value changes to a
specified value. Unlike validators, listeners cannot block the
change.
Configuration example:
Type: onValueChanged
Checked Field: Severity
Checked Value: Critical
Custom Code:
await stepForwardJumpTo(5, "Critical severity detected - escalating");
EmailReceived
Triggers when an inbound email matches regex patterns on subject
and/or body content.
Configuration example:
Type: onEmailReceived
Subject Regex: (Out of Office|Automatic reply)
Content Regex: .*
Interval: */5 * * * *
Custom Code:
console.log("Auto-reply detected");
2. Required Capabilities
A. Detecting Received Emails
The system can detect inbound emails with specific content using
regex pattern matching.
How to configure:
- Navigate to Settings > Listeners
- Add a listener with Type =
onEmailReceived
- Set Subject Regex (regex pattern to match the email
subject)
- Set Content Regex (regex pattern to match the email
body)
- Set Interval (cron expression for how often to
poll, e.g.,
*/5 * * * * for every 5 minutes)
- Write Custom Code to execute when a matching email is found
Use cases:
- Detect autoresponder / out-of-office replies:
Subject Regex: (?i)(Out of Office|Automatic reply|Abwesenheit)
- Detect approval keywords in email body:
Content Regex: (?i)(approved|confirmed)
- Trigger ticket creation from inbound emails
How it works internally:
- A cron job polls for new emails via the Microsoft Graph API at the
configured interval
- Each email is matched against the listener’s
SubjectRegex and ContentRegex
- The
ProcessedEmailListeners database table prevents
duplicate processing
- When matched, the listener’s Custom Code executes with full access
to the macro API
B. Detecting Ticket Value
Changes
Two systems detect ticket value changes:
System 1: Event Listeners
(post-change)
Listeners of type onValueChanged check if a field has a
specific value after a change.
Configuration:
| Field |
Value |
| Type |
onValueChanged |
| Checked Field |
Field name (e.g., Severity) |
| Checked Value |
Target value (e.g., Critical) |
| Workflow ID |
Target workflow (or set Default = true for all) |
Example: Route to different workflow based on
severity
// Listener: onValueChanged, CheckedField: "Severity", CheckedValue: "Critical"
await stepForwardJumpTo(5, "Escalated due to critical severity");
await sendMailTo(
["escalation-team@example.com"],
"Ticket has been escalated to critical severity.",
"Critical Escalation"
);
System 2: Field Validators
(pre-change)
Validators run before the value is saved and can
block the change.
Example: Different validation based on severity
// Validator on "Severity" field
// ErrorMessage: "Only managers can set severity to Critical"
if (getCurrentFieldValue() === "Critical") {
return hasRole("Manager");
}
return true;
C. User Rights
Custom code can access the current user’s roles to enforce
permission-based logic.
In Field Validators
The hasRole(roleName) function is available and checks
whether the current user has a specific role.
Example:
// Only users with "Editor" role can modify this field
if (!hasRole("Editor")) {
return false; // ErrorMessage: "You don't have the right to modify this value."
}
return true;
Available helper:
| Function |
Description |
hasRole(roleName) |
Returns true if the current user has the named role,
false otherwise |
Available variable:
| Variable |
Description |
userRoles |
Array of the current user’s role names (e.g.,
["Admin", "Editor"]) |
In Workflow Step
Macros and Event Listeners
User roles are not directly available in workflow
macros. The executor receives userID but roles are not
pre-loaded into the execution context.
3. Actions Within a Macro
Trigger a Jump to a
Certain Workflow Step
Custom code can override the default forward/backward step configured
in the workflow table.
stepForwardJumpTo(stepNumber,
message?)
Jumps to the specified step number as a
success/approval action. Overrides the Forward Step ID
column.
// Skip to step 5 with an activity log message
await stepForwardJumpTo(5, "Auto-approved based on priority");
stepBackwardJumpTo(stepNumber,
message?)
Jumps to the specified step number as a
failure/denial action. Overrides the Backward Step ID
column.
// Send back to step 1 for rework
await stepBackwardJumpTo(1, "Additional information required");
Note: When these functions are used, they take
priority over the Forward Step ID and Backward Step ID
values defined in the workflow table.
Refuse to Modify a Value
Field validators can prevent a value from being saved by returning
false.
How it works:
- User changes a field value
- All active validators for that field execute in order
- If any validator returns
false, the change is
blocked
- The configured Error Message is shown as a toast
notification
- The field retains its previous value
Example: Prevent unauthorized changes
// Validator on "Budget" field
// ErrorMessage: "You don't have the right to modify this value."
if (!hasRole("Finance")) {
return false;
}
return true;
Example: Conditional refusal
// Validator on "Status" field
// ErrorMessage: "Cannot close ticket without a resolution."
if (getCurrentFieldValue() === "Closed") {
const resolution = getFieldValue("Resolution");
return resolution && resolution.trim() !== "";
}
return true;
4. Misc Functions
A. Show Message
In Field Validators
When a validator returns false, the Error
Message configured on the validator is displayed to the user as
a toast notification. This is the primary mechanism for showing messages
during field validation.
How to configure:
- Navigate to Settings > Workflows >
Validators
- Create or edit a validator
- Set the Error Message field (max 500
characters)
- Write validation code that returns
false when the
message should be shown
Example:
Error Message: "You don't have the right to modify this value."
Custom Code: return hasRole("Editor");
In Workflow Step Macros
There is currently no showMessage() function available
in workflow macros. Console output via console.log() is
captured during compilation/testing but is not displayed to the user in
production mode.
B. Sending Emails
The sendMailTo() function sends emails via the Microsoft
Graph API.
Function Signature
await sendMailTo(recipients, message, subject, attachments?)
| Parameter |
Type |
Description |
recipients |
string[] |
Array of email addresses |
message |
string |
HTML email body (supports template variables) |
subject |
string |
Email subject line (supports template variables) |
attachments |
any[] (optional) |
Array of attachments (PDF reports, UPVF files) |
Basic Usage
await sendMailTo(
["d.dolinsky@caxperts.com", "daniel.dolinsky@dolinsky.de"],
"Ticket has been approved.",
"Ticket Approval Notification"
);
HTML Email Body
The message parameter accepts full HTML:
await sendMailTo(
["team@example.com"],
`<h2>Ticket Update</h2>
<p>Ticket <strong>#${await getTicketValue("PunchItem")}</strong> has been approved.</p>
<ul>
<li><strong>Status:</strong> Approved</li>
<li><strong>Date:</strong> ${Date("MMMM dd, yyyy")}</li>
</ul>
<p><a href="${serverAddress}/tickets/${UniqueID}">View Ticket</a></p>`,
"Ticket Approval Notification"
);
With PDF Attachment
const report = await createPdfReport("ReportTemplate");
await sendMailTo(
["user@example.com"],
"Please see the attached report.",
"Monthly Report",
report
);
With UPVF File Attachment
const upvfFile = await extractUpvf("drawing.upvf");
attachments.push(upvfFile);
await sendMailTo(
["user@example.com"],
"UPVF file attached.",
"Document Delivery",
attachments
);
With Multiple Attachments
(PDF + UPVF)
attachments.push(createPdfReport("Template"));
attachments.push(extractUpvf());
await sendMailTo(
["user@example.com"],
"Report and UPVF file attached.",
"Combined Delivery",
attachments
);
Note: The attachments array is
pre-defined in the execution context. You can push items to it and pass
it to sendMailTo.
Template Variables in Emails
Email message and subject support ${expression} template
variable syntax. See String
Replacements for the full list of supported variables.
Environment behavior: Emails are only sent when
NEXT_PUBLIC_PRODUCTION=1. In development mode, emails are
logged to the console instead of being sent.
C. String Replacements
The system uses ES6 template literal syntax
${expression} for dynamic variable substitution in email
templates and workflow email messages.
Supported Variables
| Pattern |
Example Output |
Description |
${UniqueID} |
123 |
Ticket unique ID |
${PunchItem} |
123 |
Ticket punch item ID |
${Details["Field Name"]} |
Broken pipe |
Any ticket field value |
${Details.Subject} |
Leak in area B |
Shorthand for field access (no spaces in name) |
${serverAddress} |
https://toolkit.example.com |
Application server URL (from NEXTAUTH_URL) |
${Date("format")} |
2025-12-01 |
Current date with format string |
${FormatDate(value, "format")} |
01/12/2025 |
Format any date value |
${ParseFormatDate(str, inFmt, outFmt)} |
2025-12-01 |
Parse a date string and reformat it |
${Workflow[1].Note} |
Manager approved |
Workflow step note (by step number) |
${Workflows[0].Status} |
Accepted |
Workflow step status (by array index, legacy) |
| Format |
Example |
yyyy-MM-dd |
2025-12-01 |
MM/dd/yyyy |
12/01/2025 |
dd-MM-yyyy |
01-12-2025 |
yyyy-MM-dd HH:mm:ss |
2025-12-01 14:30:00 |
MMMM dd, yyyy |
December 01, 2025 |
dd.MM.yyyy |
01.12.2025 |
Usage in Custom Code
The Date function is directly available in custom code.
It supports two syntaxes:
// As a template variable (calls toString, returns yyyy-MM-dd)
const msg = `Today is ${Date}`;
// As a function call (with optional format string)
await setTicketValue("ApprovalDate", Date("yyyy-MM-dd"));
await setTicketValue("Timestamp", Date("yyyy-MM-dd HH:mm:ss"));
These helper functions are available in custom code for formatting
arbitrary dates:
// FormatDate(dateInput, formatString?) - format any date value
const formatted = FormatDate("2025-12-01", "dd/MM/yyyy"); // "01/12/2025"
const fromDate = FormatDate(new Date(), "MMMM dd, yyyy"); // "February 13, 2026"
// ParseFormatDate(dateString, inputFormat, outputFormat?) - parse and reformat
const result = ParseFormatDate("01-12-2025", "dd-MM-yyyy", "yyyy/MM/dd"); // "2025/12/01"
serverAddress in Custom Code
The serverAddress and UniqueID variables
are directly available in custom code for building full ticket URLs.
Note: serverAddress is derived from
NEXTAUTH_URL and already includes the instance
path (e.g., /instance2), so you don’t need to
append it separately.
// Build full ticket URL (serverAddress already includes instance path)
const ticketUrl = `${serverAddress}/tickets/${UniqueID}`;
// Example: https://toolkit-dev.udith.io/instance2/tickets/102
await sendMailTo(
["user@example.com"],
`<p><a href="${ticketUrl}">View Ticket #${UniqueID}</a></p>`,
"Ticket Link"
);
await sendMailTo(
["team@example.com"],
`<h2>Ticket Update</h2>
<p>Ticket <strong>#${UniqueID}</strong> has been approved.</p>
<p><a href="${serverAddress}/tickets/${UniqueID}">View Ticket</a></p>`,
"Ticket Approval Notification"
);
| Function |
Parameters |
Description |
Date(format?) |
format: date-fns format string (default:
yyyy-MM-dd) |
Current date. Works as ${Date} or
Date("format") |
FormatDate(dateInput, format?) |
dateInput: Date, string, or timestamp;
format: output format (default:
yyyy-MM-dd) |
Format any date value. Returns "" on invalid input |
ParseFormatDate(str, inFmt, outFmt?) |
str: date string; inFmt: input format;
outFmt: output format (default:
yyyy-MM-dd) |
Parse a date string with known format and reformat it |
Usage in Email Templates
Template variables are used in Workflow Email Messages (configured in
the workflow table) and in sendMailTo() message/subject
parameters:
<h2>Ticket Update Notification</h2>
<p>Ticket <strong>#${PunchItem}</strong> has been updated.</p>
<ul>
<li><strong>Area System:</strong> ${Details["Area System"]}</li>
<li><strong>Date:</strong> ${Date("MMMM dd, yyyy")}</li>
</ul>
<p><a href="${serverAddress}/tickets/${PunchItem}">View Ticket</a></p>
Safe Proxies
Missing or undefined variables return an empty string instead of
throwing an error. For example, ${NonExistentField} renders
as "".
D. Ticket Read/Write
(including .upvf-Blobs)
Custom code has full read and write access to ticket data and file
attachments.
Reading Ticket Data
| Function |
Description |
await getTicketValue(fieldName) |
Returns the value of a single ticket field |
await getPunchlistDetails() |
Returns an object with all ticket field names and values |
Examples:
const tagNo = await getTicketValue("Tag No");
console.log("Tag Number:", tagNo);
const allFields = await getPunchlistDetails();
console.log("Status:", allFields["Status"]);
Writing Ticket Data
| Function |
Description |
await setTicketValue(fieldName, value) |
Sets a field value (creates if it doesn’t exist) |
await updatePunchlistDetails(data) |
Batch-updates multiple fields |
await createHiddenField(data) |
Creates a ticket-specific hidden field |
Examples:
await setTicketValue("Status Comment", "Auto-approved by macro");
await setTicketValue("ApprovalDate", Date("yyyy-MM-dd"));
await updatePunchlistDetails([
{ "Tag No": "P-12345" },
{ "Unit No": "U-001" }
]);
await createHiddenField({ "InternalFlag": "processed" });
UPVF File Operations
| Function |
Description |
await extractUpvf(fileName?) |
Extracts a UPVF file as a Buffer object |
await deleteUpvf(fileID) |
Soft-deletes a UPVF file (sets Hidden = true) |
ExtractUpvf returns an object with:
{
filename: "drawing.upvf", // File name
content: Buffer, // Binary content as Buffer
contentType: "application/json", // MIME type
fileInfo: {
ID: 42, // File ID (use with DeleteUpvf)
FileName: "drawing.upvf",
DateTimeCreated: Date
}
}
Examples:
// Extract and inspect a UPVF file
const upvf = await extractUpvf("drawing.upvf");
console.log(upvf.filename, upvf.content.length, "bytes");
// Extract and attach to email
attachments.push(extractUpvf());
await sendMailTo(["user@example.com"], "UPVF attached", "File Delivery", attachments);
// Extract, process, then delete
const upvf = await extractUpvf();
if (upvf) {
// ... process file ...
await deleteUpvf(upvf.fileInfo.ID);
}
PDF Report Generation
| Function |
Description |
await createPdfReport(templateName?, ...params) |
Generates a PDF report for the current ticket |
Example:
const report = await createPdfReport();
await sendMailTo(
["user@example.com"],
"Report attached",
"Ticket Report",
report
);
E. Reading Current Language
The current user’s language is available via the
language variable. The language is sourced from the
application’s language setting (configured per user in the application
UI). If no language is set, it defaults to "en-US".
In All Contexts
(Macros, Listeners, Validators)
console.log(language); // e.g., "en-US", "ko", "de"
const message = language === "ko" ? "승인됨" : "Approved";
await setTicketValue("StatusLabel", message);
Note: The language variable is always
defined and never undefined. It defaults to
"en-US" when no language is configured.
F. HTTP(S) POSTs and GETs
Custom code can make HTTP requests using the standard
fetch API, which is available globally in the Node.js
execution environment.
GET Request
const response = await fetch("https://api.example.com/data");
const data = await response.json();
console.log(data);
POST Request
const response = await fetch("https://api.example.com/webhook", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
ticketId: await getTicketValue("PunchItem"),
status: await getTicketValue("Status"),
}),
});
const result = await response.json();
console.log("Webhook response:", result);
const response = await fetch("https://internal-api.example.com/notify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "your-api-key",
},
body: JSON.stringify({
message: "Ticket updated",
}),
});
if (response.ok) {
console.log("Notification sent successfully");
} else {
console.log("Notification failed:", response.status);
}
Practical Example:
Sync with External System
// When a ticket is approved, notify an external ERP system
if (userAction.Result === ActionResult.Yes) {
const ticketData = {
id: await getTicketValue("PunchItem"),
tagNo: await getTicketValue("Tag No"),
status: "Approved",
approvedDate: Date("yyyy-MM-dd"),
};
try {
const response = await fetch("https://erp.example.com/api/tickets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(ticketData),
});
if (response.ok) {
await setTicketValue("ERP Sync Status", "Synced");
} else {
await setTicketValue("ERP Sync Status", "Failed: " + response.status);
}
} catch (error) {
await setTicketValue("ERP Sync Status", "Error: " + error.message);
}
}
Note: HTTP requests execute server-side. Ensure
target URLs are reachable from the server environment. Internal network
addresses and external APIs are both supported.
Field Validators
(ValueChangeValidating)
Overview
Field validators enforce custom business rules
before field values are saved. When a user changes a
ticket field, all active validators for that field execute. If any
validator returns false, the change is blocked and the
configured error message is displayed.
Navigate to: Settings > Workflows >
Validators
Validation-Specific
Functions
These functions are available only in field
validator custom code:
| Function |
Description |
getCurrentFieldValue() |
Returns the new value the user is trying to save |
getFieldValue(fieldName) |
Returns the current value of any ticket field |
getAllFields() |
Returns an object with all field names and values |
hasRole(roleName) |
Returns true if the current user has the named
role |
Available Variables
| Variable |
Description |
currentValue |
The value being validated (same as
getCurrentFieldValue()) |
fieldName |
Name of the field being validated |
punchDetails |
Object with all field values (same as
getAllFields()) |
userRoles |
Array of the current user’s role names |
language |
Current user’s language code (defaults to "en-US") |
Quick Examples
Format validation:
// Ensure uppercase only
return getCurrentFieldValue() === getCurrentFieldValue().toUpperCase();
Role-based restriction:
// Only admins can set to "Critical"
if (getCurrentFieldValue() === "Critical") {
return hasRole("Admin");
}
return true;
Cross-field validation (select both fields in the Fields
TagBox):
// End Date must be after Start Date
if (fieldName === "End Date") {
const startDate = new Date(getFieldValue("Start Date"));
const endDate = new Date(getCurrentFieldValue());
return endDate >= startDate;
}
return true;
Custom Code Editor
The integrated code editor is available in Workflows, Listeners, and
Validators pages. It provides:
- Syntax highlighting for JavaScript
- Compile button to check for syntax errors
- Test button to execute code with mock data (no side
effects)
- Function descriptions panel showing all available
APIs and their signatures
- Console output panel showing
console.log() output during testing
- PunchlistID for compilation field to test with a
specific ticket’s data
Available Functions Summary
Ticket Management
| Function |
Context |
Description |
await getTicketValue(fieldName) |
All |
Read a ticket field value |
await setTicketValue(fieldName, value) |
Macros, Listeners |
Write a ticket field value |
await getPunchlistDetails() |
All |
Get all ticket field values |
await updatePunchlistDetails(data) |
Macros, Listeners |
Batch update ticket fields |
await createHiddenField(data) |
Macros, Listeners |
Create a hidden ticket-specific field |
Workflow Control
| Function |
Context |
Description |
await setStatus(status) |
Macros, Listeners |
Set the ticket status |
await stepForwardJumpTo(step, message?) |
Macros, Listeners |
Jump forward (approval) |
await stepBackwardJumpTo(step, message?) |
Macros, Listeners |
Jump backward (denial) |
await closeWorkflow() |
Macros, Listeners |
Close the workflow and ticket |
await finishWorkflow() |
Macros, Listeners |
Mark the workflow as finished |
Communication
| Function |
Context |
Description |
await sendMailTo(to, message, subject, attachments?) |
Macros, Listeners |
Send an email |
await createComment(message) |
Macros, Listeners |
Create a ticket comment |
File Operations
| Function |
Context |
Description |
await createPdfReport(template?, ...params) |
Macros, Listeners |
Generate a PDF report |
await extractUpvf(fileName?) |
Macros, Listeners |
Extract a UPVF file |
await deleteUpvf(fileID) |
Macros, Listeners |
Soft-delete a UPVF file |
Validation (Field Validators
only)
| Function |
Context |
Description |
getCurrentFieldValue() |
Validators |
Get the value being validated |
getFieldValue(fieldName) |
Validators |
Get any field’s current value |
getAllFields() |
Validators |
Get all field values as an object |
hasRole(roleName) |
Validators |
Check if user has a role |
Utilities
| Function |
Context |
Description |
Date / Date(format?) |
All |
Current date. Use as ${Date} or
Date("format") (default: yyyy-MM-dd) |
FormatDate(dateInput, format?) |
All |
Format any date value (default: yyyy-MM-dd) |
ParseFormatDate(str, inFmt, outFmt?) |
All |
Parse a date string and reformat it |
fetch(url, options?) |
All |
Make HTTP requests (GET, POST, etc.) |
Available Objects
| Object |
Context |
Description |
userAction.Result |
Macros |
The step action result (ActionResult.Yes /
ActionResult.No) |
ActionResult |
Macros |
Enum: Yes, No, Undefined |
language |
All |
Current user’s language code (defaults to "en-US") |
serverAddress |
All |
Application server URL from NEXTAUTH_URL, includes instance
path (e.g.,
https://toolkit.example.com/instance2) |
UniqueID |
All |
Current ticket’s UniqueID (same as
this.punchlistID) |
attachments |
Macros, Listeners |
Pre-defined array for email attachments |